001    /*
002     *   Copyright (c) 2009 The JOMC Project
003     *   Copyright (c) 2005 Christian Schulte <cs@jomc.org>
004     *   All rights reserved.
005     *
006     *   Redistribution and use in source and binary forms, with or without
007     *   modification, are permitted provided that the following conditions
008     *   are met:
009     *
010     *     o Redistributions of source code must retain the above copyright
011     *       notice, this list of conditions and the following disclaimer.
012     *
013     *     o Redistributions in binary form must reproduce the above copyright
014     *       notice, this list of conditions and the following disclaimer in
015     *       the documentation and/or other materials provided with the
016     *       distribution.
017     *
018     *   THIS SOFTWARE IS PROVIDED BY THE JOMC PROJECT AND CONTRIBUTORS "AS IS"
019     *   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020     *   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021     *   PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE JOMC PROJECT OR
022     *   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023     *   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024     *   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025     *   OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026     *   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
027     *   OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
028     *   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029     *
030     *   $Id: JomcTool.java 1237 2010-01-09 20:22:54Z schulte2005 $
031     *
032     */
033    package org.jomc.tools;
034    
035    import java.io.ByteArrayInputStream;
036    import java.io.ByteArrayOutputStream;
037    import java.io.InputStreamReader;
038    import java.io.OutputStreamWriter;
039    import java.lang.ref.Reference;
040    import java.lang.ref.WeakReference;
041    import java.text.DateFormat;
042    import java.text.Format;
043    import java.text.MessageFormat;
044    import java.text.SimpleDateFormat;
045    import java.util.ArrayList;
046    import java.util.Calendar;
047    import java.util.Collections;
048    import java.util.Date;
049    import java.util.HashMap;
050    import java.util.LinkedList;
051    import java.util.List;
052    import java.util.Locale;
053    import java.util.Map;
054    import java.util.ResourceBundle;
055    import java.util.logging.Level;
056    import org.apache.commons.lang.StringEscapeUtils;
057    import org.apache.velocity.Template;
058    import org.apache.velocity.VelocityContext;
059    import org.apache.velocity.app.VelocityEngine;
060    import org.apache.velocity.exception.ResourceNotFoundException;
061    import org.apache.velocity.runtime.RuntimeConstants;
062    import org.apache.velocity.runtime.RuntimeServices;
063    import org.apache.velocity.runtime.log.LogChute;
064    import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
065    import org.jomc.model.Argument;
066    import org.jomc.model.ArgumentType;
067    import org.jomc.model.Dependency;
068    import org.jomc.model.Implementation;
069    import org.jomc.model.Message;
070    import org.jomc.model.Modules;
071    import org.jomc.model.Multiplicity;
072    import org.jomc.model.Properties;
073    import org.jomc.model.Property;
074    import org.jomc.model.Specification;
075    import org.jomc.model.SpecificationReference;
076    import org.jomc.model.Specifications;
077    import org.jomc.model.Text;
078    
079    /**
080     * Base tool class.
081     *
082     * @author <a href="mailto:cs@jomc.org">Christian Schulte</a>
083     * @version $Id: JomcTool.java 1237 2010-01-09 20:22:54Z schulte2005 $
084     */
085    public abstract class JomcTool
086    {
087    
088        /** Listener interface. */
089        public abstract static class Listener
090        {
091    
092            /**
093             * Get called on logging.
094             *
095             * @param level The level of the event.
096             * @param message The message of the event or {@code null}.
097             * @param throwable The throwable of the event or {@code null}.
098             *
099             * @throws NullPointerException if {@code level} is {@code null}.
100             */
101            public abstract void onLog( Level level, String message, Throwable throwable );
102    
103        }
104    
105        /** Empty byte array. */
106        private static final byte[] NO_BYTES =
107        {
108        };
109    
110        /** The prefix of the template location. */
111        private static final String TEMPLATE_PREFIX =
112            JomcTool.class.getPackage().getName().replace( '.', '/' ) + "/templates/";
113    
114        /** Name of the velocity classpath resource loader implementation. */
115        private static final String VELOCITY_RESOURCE_LOADER = ClasspathResourceLoader.class.getName();
116    
117        /** Constant for the default profile. */
118        private static final String DEFAULT_PROFILE = "default";
119    
120        /**
121         * Log level events are logged at by default.
122         * @see #getDefaultLogLevel()
123         */
124        private static final Level DEFAULT_LOG_LEVEL = Level.WARNING;
125    
126        /** Default log level. */
127        private static volatile Level defaultLogLevel;
128    
129        /** The modules of the instance. */
130        private Modules modules;
131    
132        /** {@code VelocityEngine} of the generator. */
133        private VelocityEngine velocityEngine;
134    
135        /** The encoding to use for reading templates. */
136        private String templateEncoding;
137    
138        /** The encoding to use for reading files. */
139        private String inputEncoding;
140    
141        /** The encoding to use for writing files. */
142        private String outputEncoding;
143    
144        /** The profile of the instance. */
145        private String profile;
146    
147        /** The listeners of the instance. */
148        private List<Listener> listeners;
149    
150        /** Log level of the instance. */
151        private Level logLevel;
152    
153        /** Cached templates. */
154        private Reference<Map<String, Template>> templateCache =
155            new WeakReference<Map<String, Template>>( new HashMap<String, Template>() );
156    
157        /** Creates a new {@code JomcTool} instance. */
158        public JomcTool()
159        {
160            super();
161        }
162    
163        /**
164         * Creates a new {@code JomcTool} instance taking a {@code JomcTool} instance to initialize the new instance with.
165         *
166         * @param tool The instance to initialize the new instance with.
167         */
168        public JomcTool( final JomcTool tool )
169        {
170            this();
171            if ( tool != null )
172            {
173                try
174                {
175                    this.setTemplateEncoding( tool.getTemplateEncoding() );
176                    this.setInputEncoding( tool.getInputEncoding() );
177                    this.setOutputEncoding( tool.getOutputEncoding() );
178                    this.setModules( tool.getModules() );
179                    this.setProfile( tool.getProfile() );
180                    this.setVelocityEngine( tool.getVelocityEngine() );
181                    this.setLogLevel( tool.getLogLevel() );
182                    this.getListeners().addAll( tool.getListeners() );
183                }
184                catch ( final Exception e )
185                {
186                    if ( this.isLoggable( Level.SEVERE ) )
187                    {
188                        this.log( Level.SEVERE, e.getMessage(), e );
189                    }
190                }
191            }
192        }
193    
194        /**
195         * Gets the list of registered listeners.
196         * <p>This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
197         * to the returned list will be present inside the object. This is why there is no {@code set} method for the
198         * listeners property.</p>
199         *
200         * @return The list of registered listeners.
201         *
202         * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
203         */
204        public List<Listener> getListeners()
205        {
206            if ( this.listeners == null )
207            {
208                this.listeners = new LinkedList<Listener>();
209            }
210    
211            return this.listeners;
212        }
213    
214        /**
215         * Gets the default log level events are logged at.
216         * <p>The default log level is controlled by system property {@code org.jomc.tools.JomcTool.defaultLogLevel} holding
217         * the log level to log events at by default. If that property is not set, the {@code WARNING} default is
218         * returned.</p>
219         *
220         * @return The log level events are logged at by default.
221         *
222         * @see #getLogLevel()
223         * @see Level#parse(java.lang.String)
224         */
225        public static Level getDefaultLogLevel()
226        {
227            if ( defaultLogLevel == null )
228            {
229                defaultLogLevel = Level.parse( System.getProperty( "org.jomc.tools.JomcTool.defaultLogLevel",
230                                                                   DEFAULT_LOG_LEVEL.getName() ) );
231    
232            }
233    
234            return defaultLogLevel;
235        }
236    
237        /**
238         * Sets the default log level events are logged at.
239         *
240         * @param value The new default level events are logged at or {@code null}.
241         *
242         * @see #getDefaultLogLevel()
243         */
244        public static void setDefaultLogLevel( final Level value )
245        {
246            defaultLogLevel = value;
247        }
248    
249        /**
250         * Gets the log level of the instance.
251         *
252         * @return The log level of the instance.
253         *
254         * @see #getDefaultLogLevel()
255         * @see #setLogLevel(java.util.logging.Level)
256         * @see #isLoggable(java.util.logging.Level)
257         */
258        public Level getLogLevel()
259        {
260            if ( this.logLevel == null )
261            {
262                this.logLevel = getDefaultLogLevel();
263                this.log( Level.CONFIG, this.getMessage( "defaultLogLevelInfo", new Object[]
264                    {
265                        this.getClass().getCanonicalName(), this.logLevel.getLocalizedName()
266                    } ), null );
267    
268            }
269    
270            return this.logLevel;
271        }
272    
273        /**
274         * Sets the log level of the instance.
275         *
276         * @param value The new log level of the instance or {@code null}.
277         *
278         * @see #getLogLevel()
279         * @see #isLoggable(java.util.logging.Level)
280         */
281        public void setLogLevel( final Level value )
282        {
283            this.logLevel = value;
284        }
285    
286        /**
287         * Checks if a message at a given level is provided to the listeners of the instance.
288         *
289         * @param level The level to test.
290         *
291         * @return {@code true} if messages at {@code level} are provided to the listeners of the instance;
292         * {@code false} if messages at {@code level} are not provided to the listeners of the instance.
293         *
294         * @throws NullPointerException if {@code level} is {@code null}.
295         *
296         * @see #getLogLevel()
297         * @see #setLogLevel(java.util.logging.Level)
298         * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
299         */
300        public boolean isLoggable( final Level level )
301        {
302            if ( level == null )
303            {
304                throw new NullPointerException( "level" );
305            }
306    
307            return level.intValue() >= this.getLogLevel().intValue();
308        }
309    
310        /**
311         * Gets the Java package name of a specification.
312         *
313         * @param specification The specification to get the Java package name of.
314         *
315         * @return The Java package name of {@code specification} or {@code null}.
316         *
317         * @throws NullPointerException if {@code specification} is {@code null}.
318         */
319        public String getJavaPackageName( final Specification specification )
320        {
321            if ( specification == null )
322            {
323                throw new NullPointerException( "specification" );
324            }
325    
326            return specification.getClazz() != null ? this.getJavaPackageName( specification.getClazz() ) : null;
327        }
328    
329        /**
330         * Gets the Java type name of a specification.
331         *
332         * @param specification The specification to get the Java type name of.
333         * @param qualified {@code true} to return the fully qualified type name (with package name prepended);
334         * {@code false} to return the short type name (without package name prepended).
335         *
336         * @return The Java type name of {@code specification} or {@code null}.
337         *
338         * @throws NullPointerException if {@code specification} is {@code null}.
339         */
340        public String getJavaTypeName( final Specification specification, final boolean qualified )
341        {
342            if ( specification == null )
343            {
344                throw new NullPointerException( "specification" );
345            }
346    
347            if ( specification.getClazz() != null )
348            {
349                final StringBuilder typeName = new StringBuilder();
350                final String javaPackageName = this.getJavaPackageName( specification );
351    
352                if ( qualified && javaPackageName.length() > 0 )
353                {
354                    typeName.append( javaPackageName ).append( '.' );
355                }
356    
357                typeName.append( javaPackageName.length() > 0
358                                 ? specification.getClazz().substring( javaPackageName.length() + 1 )
359                                 : specification.getClazz() );
360    
361                return typeName.toString();
362            }
363    
364            return null;
365        }
366    
367        /**
368         * Gets the Java class path location of a specification.
369         *
370         * @param specification The specification to return the Java class path location of.
371         *
372         * @return The Java class path location of {@code specification} or {@code null}.
373         *
374         * @throws NullPointerException if {@code specification} is {@code null}.
375         */
376        public String getJavaClasspathLocation( final Specification specification )
377        {
378            if ( specification == null )
379            {
380                throw new NullPointerException( "specification" );
381            }
382    
383            return specification.getClazz() != null
384                   ? ( this.getJavaTypeName( specification, true ) ).replace( '.', '/' )
385                   : null;
386    
387        }
388    
389        /**
390         * Gets the Java package name of a specification reference.
391         *
392         * @param reference The specification reference to get the Java package name of.
393         *
394         * @return The Java package name of {@code reference} or {@code null}.
395         *
396         * @throws NullPointerException if {@code reference} is {@code null}.
397         */
398        public String getJavaPackageName( final SpecificationReference reference )
399        {
400            if ( reference == null )
401            {
402                throw new NullPointerException( "reference" );
403            }
404    
405            final Specification s = this.getModules().getSpecification( reference.getIdentifier() );
406            assert s != null : "Specification '" + reference.getIdentifier() + "' not found.";
407            return s.getClazz() != null ? this.getJavaPackageName( s ) : null;
408        }
409    
410        /**
411         * Gets the name of a Java type of a given specification reference.
412         *
413         * @param reference The specification reference to get a Java type name of.
414         * @param qualified {@code true} to return the fully qualified type name (with package name prepended);
415         * {@code false} to return the short type name (without package name prepended).
416         *
417         * @return The Java type name of {@code reference} or {@code null}.
418         *
419         * @throws NullPointerException if {@code reference} is {@code null}.
420         */
421        public String getJavaTypeName( final SpecificationReference reference, final boolean qualified )
422        {
423            if ( reference == null )
424            {
425                throw new NullPointerException( "reference" );
426            }
427    
428            final Specification s = this.getModules().getSpecification( reference.getIdentifier() );
429            assert s != null : "Specification '" + reference.getIdentifier() + "' not found.";
430            return s.getClazz() != null ? this.getJavaTypeName( s, qualified ) : null;
431        }
432    
433        /**
434         * Gets the Java package name of an implementation.
435         *
436         * @param implementation The implementation to get the Java package name of.
437         *
438         * @return The Java package name of {@code implementation} or {@code null}.
439         *
440         * @throws NullPointerException if {@code implementation} is {@code null}.
441         */
442        public String getJavaPackageName( final Implementation implementation )
443        {
444            if ( implementation == null )
445            {
446                throw new NullPointerException( "implementation" );
447            }
448    
449            return implementation.getClazz() != null ? this.getJavaPackageName( implementation.getClazz() ) : null;
450        }
451    
452        /**
453         * Gets the Java type name of an implementation.
454         *
455         * @param implementation The implementation to get the Java type name of.
456         * @param qualified {@code true} to return the fully qualified type name (with package name prepended);
457         * {@code false} to return the short type name (without package name prepended).
458         *
459         * @return The Java type name of {@code implementation} or {@code null}.
460         *
461         * @throws NullPointerException if {@code implementation} is {@code null}.
462         */
463        public String getJavaTypeName( final Implementation implementation, final boolean qualified )
464        {
465            if ( implementation == null )
466            {
467                throw new NullPointerException( "implementation" );
468            }
469    
470            if ( implementation.getClazz() != null )
471            {
472                final StringBuilder typeName = new StringBuilder();
473                final String javaPackageName = this.getJavaPackageName( implementation );
474    
475                if ( qualified && javaPackageName.length() > 0 )
476                {
477                    typeName.append( javaPackageName ).append( '.' );
478                }
479    
480                typeName.append( javaPackageName.length() > 0
481                                 ? implementation.getClazz().substring( javaPackageName.length() + 1 )
482                                 : implementation.getClazz() );
483    
484                return typeName.toString();
485            }
486    
487            return null;
488        }
489    
490        /**
491         * Gets the Java class path location of an implementation.
492         *
493         * @param implementation The implementation to return the Java class path location of.
494         *
495         * @return The Java class path location of {@code implementation} or {@code null}.
496         *
497         * @throws NullPointerException if {@code implementation} is {@code null}.
498         */
499        public String getJavaClasspathLocation( final Implementation implementation )
500        {
501            if ( implementation == null )
502            {
503                throw new NullPointerException( "implementation" );
504            }
505    
506            return implementation.getClazz() != null
507                   ? ( this.getJavaTypeName( implementation, true ) ).replace( '.', '/' )
508                   : null;
509    
510        }
511    
512        /**
513         * Gets all Java interfaces an implementation implements.
514         *
515         * @param implementation The implementation to get all implemented Java interfaces of.
516         * @param qualified {@code true} to return the fully qualified type names (with package name prepended);
517         * {@code false} to return the short type names (without package name prepended).
518         *
519         * @return Unmodifiable list contaning all Java interfaces implemented by {@code implementation}.
520         *
521         * @throws NullPointerException if {@code implementation} is {@code null}.
522         */
523        public List<String> getJavaInterfaceNames( final Implementation implementation, final boolean qualified )
524        {
525            if ( implementation == null )
526            {
527                throw new NullPointerException( "implementation" );
528            }
529    
530            final Specifications specs = this.getModules().getSpecifications( implementation.getIdentifier() );
531            final List<String> col = new ArrayList<String>( specs == null ? 0 : specs.getSpecification().size() );
532    
533            if ( specs != null )
534            {
535                for ( Specification s : specs.getSpecification() )
536                {
537                    if ( s.getClazz() != null )
538                    {
539                        final String typeName = this.getJavaTypeName( s, qualified );
540                        if ( !col.contains( typeName ) )
541                        {
542                            col.add( typeName );
543                        }
544                    }
545                }
546            }
547    
548            return Collections.unmodifiableList( col );
549        }
550    
551        /**
552         * Gets the Java type name of an argument.
553         *
554         * @param argument The argument to get the Java type name of.
555         *
556         * @return The Java type name of {@code argument}.
557         *
558         * @throws NullPointerException if {@code argument} is {@code null}.
559         */
560        public String getJavaTypeName( final Argument argument )
561        {
562            if ( argument == null )
563            {
564                throw new NullPointerException( "argument" );
565            }
566    
567            String javaTypeName = "java.lang.String";
568    
569            if ( argument.getType() == ArgumentType.DATE || argument.getType() == ArgumentType.TIME )
570            {
571                javaTypeName = "java.util.Date";
572            }
573            else if ( argument.getType() == ArgumentType.NUMBER )
574            {
575                javaTypeName = "java.lang.Number";
576            }
577    
578            return javaTypeName;
579        }
580    
581        /**
582         * Gets the Java type name of a property.
583         *
584         * @param property The property to get the Java type name of.
585         * @param boxify {@code true} to return the name of the Java wrapper class when the type is a Java primitive type;
586         * {@code false} to return the exact binary name (unboxed name) of the Java type.
587         *
588         * @return The Java type name of {@code property}.
589         *
590         * @throws NullPointerException if {@code property} is {@code null}.
591         */
592        public String getJavaTypeName( final Property property, final boolean boxify )
593        {
594            if ( property == null )
595            {
596                throw new NullPointerException( "property" );
597            }
598    
599            if ( property.getType() != null )
600            {
601                final String typeName = property.getType();
602    
603                if ( boxify )
604                {
605                    if ( Boolean.TYPE.getName().equals( typeName ) )
606                    {
607                        return Boolean.class.getName();
608                    }
609                    if ( Byte.TYPE.getName().equals( typeName ) )
610                    {
611                        return Byte.class.getName();
612                    }
613                    if ( Character.TYPE.getName().equals( typeName ) )
614                    {
615                        return Character.class.getName();
616                    }
617                    if ( Double.TYPE.getName().equals( typeName ) )
618                    {
619                        return Double.class.getName();
620                    }
621                    if ( Float.TYPE.getName().equals( typeName ) )
622                    {
623                        return Float.class.getName();
624                    }
625                    if ( Integer.TYPE.getName().equals( typeName ) )
626                    {
627                        return Integer.class.getName();
628                    }
629                    if ( Long.TYPE.getName().equals( typeName ) )
630                    {
631                        return Long.class.getName();
632                    }
633                    if ( Short.TYPE.getName().equals( typeName ) )
634                    {
635                        return Short.class.getName();
636                    }
637                }
638    
639                return typeName;
640            }
641    
642            return property.getAny() != null ? Object.class.getName() : String.class.getName();
643        }
644    
645        /**
646         * Gets a flag indicating if the type of a given property is a Java primitive.
647         *
648         * @param property The property to query.
649         *
650         * @return {@code true} if the type of {@code property} is a Java primitive; {@code false} if not.
651         *
652         * @throws NullPointerException if {@code property} is {@code null}.
653         */
654        public boolean isJavaPrimitiveType( final Property property )
655        {
656            if ( property == null )
657            {
658                throw new NullPointerException( "property" );
659            }
660    
661            return !this.getJavaTypeName( property, false ).equals( this.getJavaTypeName( property, true ) );
662        }
663    
664        /**
665         * Gets the name of a Java accessor method of a given property.
666         *
667         * @param property The property to get a Java accessor method name of.
668         *
669         * @return The Java accessor method name of {@code property}.
670         *
671         * @throws NullPointerException if {@code property} is {@code null}.
672         */
673        public String getJavaGetterMethodName( final Property property )
674        {
675            if ( property == null )
676            {
677                throw new NullPointerException( "property" );
678            }
679    
680            final char[] name = property.getName().toCharArray();
681            name[0] = Character.toUpperCase( name[0] );
682            String prefix = "get";
683    
684            final String javaTypeName = this.getJavaTypeName( property, true );
685            if ( Boolean.class.getName().equals( javaTypeName ) )
686            {
687                prefix = "is";
688            }
689    
690            return prefix + String.valueOf( name );
691        }
692    
693        /**
694         * Gets the name of a Java type of a given dependency.
695         *
696         * @param dependency The dependency to get a dependency Java type name of.
697         *
698         * @return The Java type name of {@code dependency} or {@code null}.
699         *
700         * @throws NullPointerException if {@code dependency} is {@code null}.
701         */
702        public String getJavaTypeName( final Dependency dependency )
703        {
704            if ( dependency == null )
705            {
706                throw new NullPointerException( "dependency" );
707            }
708    
709            final Specification s = this.getModules().getSpecification( dependency.getIdentifier() );
710    
711            if ( s != null && s.getClazz() != null )
712            {
713                final StringBuilder typeName = new StringBuilder();
714                typeName.append( this.getJavaTypeName( s, true ) );
715                if ( s.getMultiplicity() == Multiplicity.MANY && dependency.getImplementationName() == null )
716                {
717                    typeName.append( "[]" );
718                }
719    
720                return typeName.toString();
721            }
722    
723            return null;
724        }
725    
726        /**
727         * Gets the name of a Java accessor method of a given dependency.
728         *
729         * @param dependency The dependency to get a Java accessor method name of.
730         *
731         * @return The Java accessor method name of {@code dependency}.
732         *
733         * @throws NullPointerException if {@code dependency} is {@code null}.
734         */
735        public String getJavaGetterMethodName( final Dependency dependency )
736        {
737            if ( dependency == null )
738            {
739                throw new NullPointerException( "dependency" );
740            }
741    
742            final char[] name = dependency.getName().toCharArray();
743            name[0] = Character.toUpperCase( name[0] );
744            return "get" + String.valueOf( name );
745        }
746    
747        /**
748         * Gets the name of a Java accessor method of a given message.
749         *
750         * @param message The message to get a Java accessor method name of.
751         *
752         * @return The Java accessor method name of {@code message}.
753         *
754         * @throws NullPointerException if {@code message} is {@code null}.
755         */
756        public String getJavaGetterMethodName( final Message message )
757        {
758            if ( message == null )
759            {
760                throw new NullPointerException( "message" );
761            }
762    
763            final char[] name = message.getName().toCharArray();
764            name[0] = Character.toUpperCase( name[0] );
765            return "get" + String.valueOf( name ) + "Message";
766        }
767    
768        /**
769         * Gets the name of a Java modifier of a dependency of a given implementation.
770         *
771         * @param implementation The implementation to get a dependency Java modifier name of.
772         * @param dependency The dependency to get a Java modifier name of.
773         *
774         * @return The Java modifier name of {@code dependency} of {@code implementation}.
775         *
776         * @throws NullPointerException if {@code implementation} or {@code dependency} is {@code null}.
777         */
778        public String getJavaModifierName( final Implementation implementation, final Dependency dependency )
779        {
780            if ( implementation == null )
781            {
782                throw new NullPointerException( "implementation" );
783            }
784            if ( dependency == null )
785            {
786                throw new NullPointerException( "dependency" );
787            }
788    
789            return "private";
790        }
791    
792        /**
793         * Gets the name of a Java modifier of a message of a given implementation.
794         *
795         * @param implementation The implementation to get a message Java modifier name of.
796         * @param message The message to get a Java modifier name of.
797         *
798         * @return The Java modifier name of {@code message} of {@code implementation}.
799         *
800         * @throws NullPointerException if {@code implementation} or {@code message} is {@code null}.
801         */
802        public String getJavaModifierName( final Implementation implementation, final Message message )
803        {
804            if ( implementation == null )
805            {
806                throw new NullPointerException( "implementation" );
807            }
808            if ( message == null )
809            {
810                throw new NullPointerException( "message" );
811            }
812    
813            return "private";
814        }
815    
816        /**
817         * Gets the name of a Java modifier for a given property of a given implementation.
818         *
819         * @param implementation The implementation declaring {@code property}.
820         * @param property The property to get a Java modifier name for.
821         *
822         * @return The Java modifier name for {@code property} of {@code implementation}.
823         *
824         * @throws NullPointerException if {@code implementation} or {@code property} is {@code null}.
825         */
826        public String getJavaModifierName( final Implementation implementation, final Property property )
827        {
828            if ( implementation == null )
829            {
830                throw new NullPointerException( "implementation" );
831            }
832            if ( property == null )
833            {
834                throw new NullPointerException( "property" );
835            }
836    
837            String modifier = "private";
838            final Properties specified = this.getModules().getSpecifiedProperties( implementation.getIdentifier() );
839    
840            if ( specified != null && specified.getProperty( property.getName() ) != null )
841            {
842                modifier = "public";
843            }
844    
845            return modifier;
846        }
847    
848        /**
849         * Formats a text to a Javadoc comment.
850         *
851         * @param text The text to format to a Javadoc comment.
852         * @param linebreak The text to replace line breaks with.
853         *
854         * @return {@code text} formatted as a Javadoc comment.
855         *
856         * @throws NullPointerException if {@code text} or {@code linebreak} is {@code null}.
857         */
858        public String getJavadocComment( final Text text, final String linebreak )
859        {
860            if ( text == null )
861            {
862                throw new NullPointerException( "text" );
863            }
864            if ( linebreak == null )
865            {
866                throw new NullPointerException( "linebreak" );
867            }
868    
869            String normalized = text.getValue();
870            normalized = normalized.replaceAll( "\\/\\*\\*", "/*" );
871            normalized = normalized.replaceAll( "\\*/", "/" );
872            normalized = normalized.replaceAll( "\n", "\n" + linebreak );
873            return StringEscapeUtils.escapeHtml( normalized );
874        }
875    
876        /**
877         * Formats a string to a Java string with unicode escapes.
878         *
879         * @param str The string to format to a Java string or {@code null}.
880         *
881         * @return {@code str} formatted as a Java string or {@code null}.
882         */
883        public String getJavaString( final String str )
884        {
885            return StringEscapeUtils.escapeJava( str );
886        }
887    
888        /**
889         * Gets a flag indicating if the class of a given specification is located in the Java default package.
890         *
891         * @param specification The specification to test.
892         *
893         * @return {@code true} if the class of {@code specification} is located in the Java default package; {@code false}
894         * if not.
895         *
896         * @throws NullPointerException if {@code specification} is {@code null}.
897         */
898        public boolean isJavaDefaultPackage( final Specification specification )
899        {
900            if ( specification == null )
901            {
902                throw new NullPointerException( "specification" );
903            }
904    
905            return specification.getClazz() != null && this.getJavaPackageName( specification ).length() == 0;
906        }
907    
908        /**
909         * Gets a flag indicating if the class of a given implementation is located in the Java default package.
910         *
911         * @param implementation The implementation to test.
912         *
913         * @return {@code true} if the class of {@code implementation} is located in the Java default package; {@code false}
914         * if not.
915         *
916         * @throws NullPointerException if {@code implementation} is {@code null}.
917         */
918        public boolean isJavaDefaultPackage( final Implementation implementation )
919        {
920            if ( implementation == null )
921            {
922                throw new NullPointerException( "implementation" );
923            }
924    
925            return implementation.getClazz() != null && this.getJavaPackageName( implementation ).length() == 0;
926        }
927    
928        /**
929         * Gets the display language of a given language code.
930         *
931         * @param language The language code to get the display language of.
932         *
933         * @return The display language of {@code language}.
934         *
935         * @throws NullPointerException if {@code language} is {@code null}.
936         */
937        public String getDisplayLanguage( final String language )
938        {
939            if ( language == null )
940            {
941                throw new NullPointerException( "language" );
942            }
943    
944            final Locale locale = new Locale( language );
945            return locale.getDisplayLanguage( locale );
946        }
947    
948        /**
949         * Formats a calendar instance to a string.
950         *
951         * @param calendar The calendar to format.
952         *
953         * @return Date of {@code calendar} formatted using a short format style pattern.
954         *
955         * @throws NullPointerException if {@code calendar} is {@code null}.
956         *
957         * @see DateFormat#SHORT
958         */
959        public String getShortDate( final Calendar calendar )
960        {
961            if ( calendar == null )
962            {
963                throw new NullPointerException( "calendar" );
964            }
965    
966            return DateFormat.getDateInstance( DateFormat.SHORT ).format( calendar.getTime() );
967        }
968    
969        /**
970         * Formats a calendar instance to a string.
971         *
972         * @param calendar The calendar to format.
973         *
974         * @return Date of {@code calendar} formatted using a long format style pattern.
975         *
976         * @throws NullPointerException if {@code calendar} is {@code null}.
977         *
978         * @see DateFormat#LONG
979         */
980        public String getLongDate( final Calendar calendar )
981        {
982            if ( calendar == null )
983            {
984                throw new NullPointerException( "calendar" );
985            }
986    
987            return DateFormat.getDateInstance( DateFormat.LONG ).format( calendar.getTime() );
988        }
989    
990        /**
991         * Formats a calendar instance to a string.
992         *
993         * @param calendar The calendar to format.
994         *
995         * @return Time of {@code calendar} formatted using a short format style pattern.
996         *
997         * @throws NullPointerException if {@code calendar} is {@code null}.
998         *
999         * @see DateFormat#SHORT
1000         */
1001        public String getShortTime( final Calendar calendar )
1002        {
1003            if ( calendar == null )
1004            {
1005                throw new NullPointerException( "calendar" );
1006            }
1007    
1008            return DateFormat.getTimeInstance( DateFormat.SHORT ).format( calendar.getTime() );
1009        }
1010    
1011        /**
1012         * Formats a calendar instance to a string.
1013         *
1014         * @param calendar The calendar to format.
1015         *
1016         * @return Time of {@code calendar} formatted using a long format style pattern.
1017         *
1018         * @throws NullPointerException if {@code calendar} is {@code null}.
1019         *
1020         * @see DateFormat#LONG
1021         */
1022        public String getLongTime( final Calendar calendar )
1023        {
1024            if ( calendar == null )
1025            {
1026                throw new NullPointerException( "calendar" );
1027            }
1028    
1029            return DateFormat.getTimeInstance( DateFormat.LONG ).format( calendar.getTime() );
1030        }
1031    
1032        /**
1033         * Formats a calendar instance to a string.
1034         *
1035         * @param calendar The calendar to format.
1036         *
1037         * @return Date and time of {@code calendar} formatted using a short format style pattern.
1038         *
1039         * @throws NullPointerException if {@code calendar} is {@code null}.
1040         *
1041         * @see DateFormat#SHORT
1042         */
1043        public String getShortDateTime( final Calendar calendar )
1044        {
1045            if ( calendar == null )
1046            {
1047                throw new NullPointerException( "calendar" );
1048            }
1049    
1050            return DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT ).format( calendar.getTime() );
1051        }
1052    
1053        /**
1054         * Formats a calendar instance to a string.
1055         *
1056         * @param calendar The calendar to format.
1057         *
1058         * @return Date and time of {@code calendar} formatted using a long format style pattern.
1059         *
1060         * @throws NullPointerException if {@code calendar} is {@code null}.
1061         *
1062         * @see DateFormat#LONG
1063         */
1064        public String getLongDateTime( final Calendar calendar )
1065        {
1066            if ( calendar == null )
1067            {
1068                throw new NullPointerException( "calendar" );
1069            }
1070    
1071            return DateFormat.getDateTimeInstance( DateFormat.LONG, DateFormat.LONG ).format( calendar.getTime() );
1072        }
1073    
1074        /**
1075         * Gets a string describing the range of years for given calendars.
1076         *
1077         * @param start The start of the range.
1078         * @param end The end of the range.
1079         *
1080         * @return Formatted range of the years of {@code start} and {@code end}.
1081         *
1082         * @throws NullPointerException if {@code start} or {@code end} is {@code null}.
1083         */
1084        public String getYears( final Calendar start, final Calendar end )
1085        {
1086            if ( start == null )
1087            {
1088                throw new NullPointerException( "start" );
1089            }
1090            if ( end == null )
1091            {
1092                throw new NullPointerException( "end" );
1093            }
1094    
1095            final Format yearFormat = new SimpleDateFormat( "yyyy" );
1096            final int s = start.get( Calendar.YEAR );
1097            final int e = end.get( Calendar.YEAR );
1098            final StringBuilder years = new StringBuilder();
1099    
1100            if ( s != e )
1101            {
1102                if ( s < e )
1103                {
1104                    years.append( yearFormat.format( start.getTime() ) ).append( " - " ).
1105                        append( yearFormat.format( end.getTime() ) );
1106    
1107                }
1108                else
1109                {
1110                    years.append( yearFormat.format( end.getTime() ) ).append( " - " ).
1111                        append( yearFormat.format( start.getTime() ) );
1112    
1113                }
1114            }
1115            else
1116            {
1117                years.append( yearFormat.format( start.getTime() ) );
1118            }
1119    
1120            return years.toString();
1121        }
1122    
1123        /**
1124         * Gets the modules of the instance.
1125         *
1126         * @return The modules of the instance.
1127         *
1128         * @see #setModules(org.jomc.model.Modules)
1129         */
1130        public Modules getModules()
1131        {
1132            if ( this.modules == null )
1133            {
1134                this.modules = new Modules();
1135            }
1136    
1137            return this.modules;
1138        }
1139    
1140        /**
1141         * Sets the modules of the instance.
1142         *
1143         * @param value The new modules of the instance.
1144         *
1145         * @see #getModules()
1146         */
1147        public void setModules( final Modules value )
1148        {
1149            this.modules = value;
1150        }
1151    
1152        /**
1153         * Gets the {@code VelocityEngine} used for generating source code.
1154         *
1155         * @return The {@code VelocityEngine} used for generating source code.
1156         *
1157         * @throws ToolException if initializing a new velocity engine fails.
1158         *
1159         * @see #setVelocityEngine(org.apache.velocity.app.VelocityEngine)
1160         */
1161        public VelocityEngine getVelocityEngine() throws ToolException
1162        {
1163            if ( this.velocityEngine == null )
1164            {
1165                try
1166                {
1167                    final java.util.Properties props = new java.util.Properties();
1168                    props.put( "resource.loader", "class" );
1169                    props.put( "class.resource.loader.class", VELOCITY_RESOURCE_LOADER );
1170                    props.put( "runtime.references.strict", Boolean.TRUE.toString() );
1171    
1172                    final VelocityEngine engine = new VelocityEngine();
1173                    engine.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new LogChute()
1174                    {
1175    
1176                        public void init( final RuntimeServices runtimeServices ) throws Exception
1177                        {
1178                        }
1179    
1180                        public void log( final int level, final String message )
1181                        {
1182                            this.log( level, message, null );
1183                        }
1184    
1185                        public void log( final int level, final String message, final Throwable throwable )
1186                        {
1187                            JomcTool.this.log( this.toToolLevel( level ), message, throwable );
1188                        }
1189    
1190                        public boolean isLevelEnabled( final int level )
1191                        {
1192                            return isLoggable( this.toToolLevel( level ) );
1193                        }
1194    
1195                        private Level toToolLevel( final int logChuteLevel )
1196                        {
1197                            switch ( logChuteLevel )
1198                            {
1199                                case LogChute.DEBUG_ID:
1200                                    return Level.FINE;
1201    
1202                                case LogChute.ERROR_ID:
1203                                    return Level.SEVERE;
1204    
1205                                case LogChute.INFO_ID:
1206                                    return Level.INFO;
1207    
1208                                case LogChute.TRACE_ID:
1209                                    return Level.FINER;
1210    
1211                                case LogChute.WARN_ID:
1212                                    return Level.WARNING;
1213    
1214                                default:
1215                                    return Level.FINEST;
1216    
1217                            }
1218                        }
1219    
1220                    } );
1221    
1222                    engine.init( props );
1223                    this.velocityEngine = engine;
1224                    this.templateCache.clear();
1225                }
1226                catch ( final Exception e )
1227                {
1228                    throw new ToolException( e );
1229                }
1230            }
1231    
1232            return this.velocityEngine;
1233        }
1234    
1235        /**
1236         * Sets the {@code VelocityEngine} of the instance.
1237         *
1238         * @param value The new {@code VelocityEngine} of the instance.
1239         *
1240         * @see #getVelocityEngine()
1241         */
1242        public void setVelocityEngine( final VelocityEngine value )
1243        {
1244            this.velocityEngine = value;
1245        }
1246    
1247        /**
1248         * Gets the velocity context used for merging templates.
1249         *
1250         * @return The velocity context used for merging templates.
1251         */
1252        public VelocityContext getVelocityContext()
1253        {
1254            final Date now = new Date();
1255            final VelocityContext ctx = new VelocityContext();
1256            ctx.put( "modules", this.getModules() );
1257            ctx.put( "tool", this );
1258            ctx.put( "calendar", Calendar.getInstance() );
1259            ctx.put( "now", new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ).format( now ) );
1260            ctx.put( "year", new SimpleDateFormat( "yyyy" ).format( now ) );
1261            ctx.put( "month", new SimpleDateFormat( "MM" ).format( now ) );
1262            ctx.put( "day", new SimpleDateFormat( "dd" ).format( now ) );
1263            ctx.put( "hour", new SimpleDateFormat( "HH" ).format( now ) );
1264            ctx.put( "minute", new SimpleDateFormat( "mm" ).format( now ) );
1265            ctx.put( "second", new SimpleDateFormat( "ss" ).format( now ) );
1266            ctx.put( "timezone", new SimpleDateFormat( "Z" ).format( now ) );
1267            return ctx;
1268        }
1269    
1270        /**
1271         * Gets the encoding to use for reading templates.
1272         *
1273         * @return The encoding to use for reading templates.
1274         *
1275         * @see #setTemplateEncoding(java.lang.String)
1276         */
1277        public String getTemplateEncoding()
1278        {
1279            if ( this.templateEncoding == null )
1280            {
1281                this.templateEncoding = this.getMessage( "buildSourceEncoding", null );
1282                this.templateCache.clear();
1283                if ( this.isLoggable( Level.FINE ) )
1284                {
1285                    this.log( Level.FINE, this.getMessage( "defaultTemplateEncoding", new Object[]
1286                        {
1287                            this.templateEncoding
1288                        } ), null );
1289    
1290                }
1291            }
1292    
1293            return this.templateEncoding;
1294        }
1295    
1296        /**
1297         * Sets the encoding to use for reading templates.
1298         *
1299         * @param value The encoding to use for reading templates.
1300         *
1301         * @see #getTemplateEncoding()
1302         */
1303        public void setTemplateEncoding( final String value )
1304        {
1305            this.templateEncoding = value;
1306            this.templateCache.clear();
1307        }
1308    
1309        /**
1310         * Gets the encoding to use for reading files.
1311         *
1312         * @return The encoding to use for reading files.
1313         *
1314         * @see #setInputEncoding(java.lang.String)
1315         */
1316        public String getInputEncoding()
1317        {
1318            if ( this.inputEncoding == null )
1319            {
1320                this.inputEncoding = new InputStreamReader( new ByteArrayInputStream( NO_BYTES ) ).getEncoding();
1321                if ( this.isLoggable( Level.FINE ) )
1322                {
1323                    this.log( Level.FINE, this.getMessage( "defaultInputEncoding", new Object[]
1324                        {
1325                            this.inputEncoding
1326                        } ), null );
1327    
1328                }
1329            }
1330    
1331            return this.inputEncoding;
1332        }
1333    
1334        /**
1335         * Sets the encoding to use for reading files.
1336         *
1337         * @param value The encoding to use for reading files.
1338         *
1339         * @see #getInputEncoding()
1340         */
1341        public void setInputEncoding( final String value )
1342        {
1343            this.inputEncoding = value;
1344        }
1345    
1346        /**
1347         * Gets the encoding to use for writing files.
1348         *
1349         * @return The encoding to use for writing files.
1350         *
1351         * @see #setOutputEncoding(java.lang.String)
1352         */
1353        public String getOutputEncoding()
1354        {
1355            if ( this.outputEncoding == null )
1356            {
1357                this.outputEncoding = new OutputStreamWriter( new ByteArrayOutputStream() ).getEncoding();
1358                if ( this.isLoggable( Level.FINE ) )
1359                {
1360                    this.log( Level.FINE, this.getMessage( "defaultOutputEncoding", new Object[]
1361                        {
1362                            this.outputEncoding
1363                        } ), null );
1364    
1365                }
1366            }
1367    
1368            return this.outputEncoding;
1369        }
1370    
1371        /**
1372         * Sets the encoding to use for writing files.
1373         *
1374         * @param value The encoding to use for writing files.
1375         *
1376         * @see #getOutputEncoding()
1377         */
1378        public void setOutputEncoding( final String value )
1379        {
1380            this.outputEncoding = value;
1381        }
1382    
1383        /**
1384         * Gets the profile of the instance.
1385         *
1386         * @return The profile of the instance.
1387         *
1388         * @see #setProfile(java.lang.String)
1389         */
1390        public String getProfile()
1391        {
1392            if ( this.profile == null )
1393            {
1394                this.profile = DEFAULT_PROFILE;
1395                if ( this.isLoggable( Level.FINE ) )
1396                {
1397                    this.log( Level.FINE, this.getMessage( "defaultProfile", new Object[]
1398                        {
1399                            this.profile
1400                        } ), null );
1401    
1402                }
1403            }
1404    
1405            return this.profile;
1406        }
1407    
1408        /**
1409         * Sets the profile of the instance.
1410         *
1411         * @param value The profile of the instance.
1412         *
1413         * @see #getProfile()
1414         */
1415        public void setProfile( final String value )
1416        {
1417            this.profile = value;
1418            this.templateCache.clear();
1419        }
1420    
1421        /**
1422         * Gets a velocity template for a given name.
1423         * <p>This method returns the template corresponding to the profile of the instance. If that template is not found,
1424         * the template of the default profile is returned so that only templates differing from the default templates need
1425         * to be provided when exchanging templates.</p>
1426         *
1427         * @param templateName The name of the template to get.
1428         *
1429         * @return The template matching {@code templateName}.
1430         *
1431         * @throws NullPointerException if {@code templateName} is {@code null}.
1432         * @throws ToolException if getting the template fails.
1433         *
1434         * @see #getProfile()
1435         * @see #getTemplateEncoding()
1436         */
1437        public Template getVelocityTemplate( final String templateName ) throws ToolException
1438        {
1439            if ( templateName == null )
1440            {
1441                throw new NullPointerException( "templateName" );
1442            }
1443    
1444            Map<String, Template> templates = this.templateCache.get();
1445            if ( templates == null )
1446            {
1447                templates = new HashMap<String, Template>();
1448                this.templateCache = new WeakReference<Map<String, Template>>( templates );
1449            }
1450    
1451            Template template = templates.get( templateName );
1452    
1453            if ( template == null )
1454            {
1455                try
1456                {
1457                    template = this.getVelocityEngine().getTemplate(
1458                        TEMPLATE_PREFIX + this.getProfile() + "/" + templateName, this.getTemplateEncoding() );
1459    
1460                    templates.put( templateName, template );
1461                }
1462                catch ( final ResourceNotFoundException e )
1463                {
1464                    if ( this.isLoggable( Level.CONFIG ) )
1465                    {
1466                        this.log( Level.CONFIG, this.getMessage( "templateNotFound", new Object[]
1467                            {
1468                                templateName, this.getProfile()
1469                            } ), e );
1470    
1471                    }
1472    
1473                    try
1474                    {
1475                        template = this.getVelocityEngine().getTemplate(
1476                            TEMPLATE_PREFIX + DEFAULT_PROFILE + "/" + templateName, this.getTemplateEncoding() );
1477    
1478                        if ( this.isLoggable( Level.CONFIG ) )
1479                        {
1480                            this.log( Level.CONFIG, this.getMessage( "defaultTemplate", new Object[]
1481                                {
1482                                    templateName, DEFAULT_PROFILE
1483                                } ), e );
1484    
1485                        }
1486    
1487                        templates.put( templateName, template );
1488                    }
1489                    catch ( final ResourceNotFoundException e2 )
1490                    {
1491                        throw new ToolException( this.getMessage( "templateNotFound", new Object[]
1492                            {
1493                                templateName, DEFAULT_PROFILE
1494                            } ), e2 );
1495    
1496                    }
1497                    catch ( final Exception e2 )
1498                    {
1499                        throw new ToolException( this.getMessage( "failedGettingTemplate", new Object[]
1500                            {
1501                                templateName
1502                            } ), e2 );
1503    
1504                    }
1505                }
1506                catch ( final Exception e )
1507                {
1508                    throw new ToolException( this.getMessage( "failedGettingTemplate", new Object[]
1509                        {
1510                            templateName
1511                        } ), e );
1512    
1513                }
1514            }
1515    
1516            return template;
1517        }
1518    
1519        /**
1520         * Notifies registered listeners.
1521         *
1522         * @param level The level of the event.
1523         * @param message The message of the event or {@code null}.
1524         * @param throwable The throwable of the event or {@code null}.
1525         *
1526         * @throws NullPointerException if {@code level} is {@code null}.
1527         *
1528         * @see #getListeners()
1529         */
1530        protected void log( final Level level, final String message, final Throwable throwable )
1531        {
1532            if ( level == null )
1533            {
1534                throw new NullPointerException( "level" );
1535            }
1536    
1537            if ( this.isLoggable( level ) )
1538            {
1539                for ( Listener l : this.getListeners() )
1540                {
1541                    l.onLog( level, message, throwable );
1542                }
1543            }
1544        }
1545    
1546        private String getJavaPackageName( final String identifier )
1547        {
1548            if ( identifier == null )
1549            {
1550                throw new NullPointerException( "identifier" );
1551            }
1552    
1553            final int idx = identifier.lastIndexOf( '.' );
1554            return idx != -1 ? identifier.substring( 0, idx ) : "";
1555        }
1556    
1557        private String getMessage( final String key, final Object args )
1558        {
1559            if ( key == null )
1560            {
1561                throw new NullPointerException( "key" );
1562            }
1563    
1564            final ResourceBundle b = ResourceBundle.getBundle( JomcTool.class.getName().replace( '.', '/' ) );
1565            return args == null ? b.getString( key ) : new MessageFormat( b.getString( key ) ).format( args );
1566        }
1567    
1568    }