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