001    /*
002     *   Copyright (C) Christian Schulte, 2005-206
003     *   All rights reserved.
004     *
005     *   Redistribution and use in source and binary forms, with or without
006     *   modification, are permitted provided that the following conditions
007     *   are met:
008     *
009     *     o Redistributions of source code must retain the above copyright
010     *       notice, this list of conditions and the following disclaimer.
011     *
012     *     o Redistributions in binary form must reproduce the above copyright
013     *       notice, this list of conditions and the following disclaimer in
014     *       the documentation and/or other materials provided with the
015     *       distribution.
016     *
017     *   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
018     *   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
019     *   AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
020     *   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
021     *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
022     *   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
023     *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
024     *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025     *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
026     *   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027     *
028     *   $JOMC: JomcTool.java 4539 2012-05-23 03:04:49Z schulte2005 $
029     *
030     */
031    package org.jomc.tools;
032    
033    import java.io.BufferedReader;
034    import java.io.ByteArrayInputStream;
035    import java.io.ByteArrayOutputStream;
036    import java.io.FileNotFoundException;
037    import java.io.IOException;
038    import java.io.InputStream;
039    import java.io.InputStreamReader;
040    import java.io.OutputStreamWriter;
041    import java.io.Reader;
042    import java.io.StringReader;
043    import java.lang.ref.Reference;
044    import java.lang.ref.SoftReference;
045    import java.lang.reflect.InvocationTargetException;
046    import java.net.URL;
047    import java.text.DateFormat;
048    import java.text.Format;
049    import java.text.MessageFormat;
050    import java.text.SimpleDateFormat;
051    import java.util.ArrayList;
052    import java.util.Calendar;
053    import java.util.Collections;
054    import java.util.Enumeration;
055    import java.util.HashMap;
056    import java.util.List;
057    import java.util.Locale;
058    import java.util.Map;
059    import java.util.ResourceBundle;
060    import java.util.Set;
061    import java.util.concurrent.ConcurrentHashMap;
062    import java.util.concurrent.CopyOnWriteArrayList;
063    import java.util.concurrent.CopyOnWriteArraySet;
064    import java.util.logging.Level;
065    import javax.activation.MimeType;
066    import javax.activation.MimeTypeParseException;
067    import org.apache.commons.io.IOUtils;
068    import org.apache.commons.lang.StringEscapeUtils;
069    import org.apache.commons.lang.StringUtils;
070    import org.apache.velocity.Template;
071    import org.apache.velocity.VelocityContext;
072    import org.apache.velocity.app.VelocityEngine;
073    import org.apache.velocity.exception.ParseErrorException;
074    import org.apache.velocity.exception.ResourceNotFoundException;
075    import org.apache.velocity.exception.VelocityException;
076    import org.apache.velocity.runtime.RuntimeConstants;
077    import org.apache.velocity.runtime.RuntimeServices;
078    import org.apache.velocity.runtime.log.LogChute;
079    import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
080    import org.apache.velocity.runtime.resource.loader.URLResourceLoader;
081    import org.jomc.model.Argument;
082    import org.jomc.model.ArgumentType;
083    import org.jomc.model.Dependency;
084    import org.jomc.model.Implementation;
085    import org.jomc.model.InheritanceModel;
086    import org.jomc.model.Message;
087    import org.jomc.model.ModelObject;
088    import org.jomc.model.Modules;
089    import org.jomc.model.Multiplicity;
090    import org.jomc.model.Properties;
091    import org.jomc.model.Property;
092    import org.jomc.model.Specification;
093    import org.jomc.model.SpecificationReference;
094    import org.jomc.model.Specifications;
095    import org.jomc.model.Text;
096    import org.jomc.model.Texts;
097    import org.jomc.model.modlet.ModelHelper;
098    import org.jomc.modlet.Model;
099    
100    /**
101     * Base tool class.
102     *
103     * @author <a href="mailto:schulte2005@users.sourceforge.net">Christian Schulte</a>
104     * @version $JOMC: JomcTool.java 4539 2012-05-23 03:04:49Z schulte2005 $
105     */
106    public class JomcTool
107    {
108    
109        /** Listener interface. */
110        public abstract static class Listener
111        {
112    
113            /** Creates a new {@code Listener} instance. */
114            public Listener()
115            {
116                super();
117            }
118    
119            /**
120             * Gets called on logging.
121             *
122             * @param level The level of the event.
123             * @param message The message of the event or {@code null}.
124             * @param throwable The throwable of the event or {@code null}.
125             *
126             * @throws NullPointerException if {@code level} is {@code null}.
127             */
128            public void onLog( final Level level, final String message, final Throwable throwable )
129            {
130                if ( level == null )
131                {
132                    throw new NullPointerException( "level" );
133                }
134            }
135    
136        }
137    
138        /** Empty byte array. */
139        private static final byte[] NO_BYTES =
140        {
141        };
142    
143        /** The prefix of the template location. */
144        private static final String TEMPLATE_PREFIX =
145            JomcTool.class.getPackage().getName().replace( '.', '/' ) + "/templates/";
146    
147        /** Constant for the default template profile. */
148        private static final String DEFAULT_TEMPLATE_PROFILE = "jomc-java";
149    
150        /** The default template profile. */
151        private static volatile String defaultTemplateProfile;
152    
153        /**
154         * The log level events are logged at by default.
155         * @see #getDefaultLogLevel()
156         */
157        private static final Level DEFAULT_LOG_LEVEL = Level.WARNING;
158    
159        /** The default log level. */
160        private static volatile Level defaultLogLevel;
161    
162        /** The model of the instance. */
163        private Model model;
164    
165        /** The {@code VelocityEngine} of the instance. */
166        private VelocityEngine velocityEngine;
167    
168        /**
169         * Flag indicating the default {@code VelocityEngine}.
170         * @since 1.2.4
171         */
172        private boolean defaultVelocityEngine;
173    
174        /** The encoding to use for reading templates. */
175        private String templateEncoding;
176    
177        /**
178         * The location to search for templates in addition to searching the class path.
179         * @since 1.2
180         */
181        private URL templateLocation;
182    
183        /** The encoding to use for reading files. */
184        private String inputEncoding;
185    
186        /** The encoding to use for writing files. */
187        private String outputEncoding;
188    
189        /**
190         * The template parameters.
191         * @since 1.2
192         */
193        private Map<String, Object> templateParameters;
194    
195        /** The template profile of the instance. */
196        private String templateProfile;
197    
198        /** The indentation string of the instance. */
199        private String indentation;
200    
201        /** The line separator of the instance. */
202        private String lineSeparator;
203    
204        /** The listeners of the instance. */
205        private List<Listener> listeners;
206    
207        /** The log level of the instance. */
208        private Level logLevel;
209    
210        /**
211         * The locale of the instance.
212         * @since 1.2
213         */
214        private Locale locale;
215    
216        /** Cached indentation strings. */
217        private volatile Reference<Map<String, String>> indentationCache;
218    
219        /** Cached template locations. */
220        private volatile Reference<Map<String, String>> templateLocationsCache;
221    
222        /** Cached template profile properties. */
223        private volatile Reference<Map<String, java.util.Properties>> templateProfilePropertiesCache;
224    
225        /** Cached Java keywords. */
226        private volatile Reference<Set<String>> javaKeywordsCache;
227    
228        /** Creates a new {@code JomcTool} instance. */
229        public JomcTool()
230        {
231            super();
232        }
233    
234        /**
235         * Creates a new {@code JomcTool} instance taking a {@code JomcTool} instance to initialize the new instance with.
236         *
237         * @param tool The instance to initialize the new instance with.
238         *
239         * @throws NullPointerException if {@code tool} is {@code null}.
240         * @throws IOException if copying {@code tool} fails.
241         */
242        public JomcTool( final JomcTool tool ) throws IOException
243        {
244            this();
245    
246            if ( tool == null )
247            {
248                throw new NullPointerException( "tool" );
249            }
250    
251            this.indentation = tool.indentation;
252            this.inputEncoding = tool.inputEncoding;
253            this.lineSeparator = tool.lineSeparator;
254            this.listeners = tool.listeners != null ? new CopyOnWriteArrayList<Listener>( tool.listeners ) : null;
255            this.logLevel = tool.logLevel;
256            this.model = tool.model != null ? tool.model.clone() : null;
257            this.outputEncoding = tool.outputEncoding;
258            this.templateEncoding = tool.templateEncoding;
259            this.templateProfile = tool.templateProfile;
260            this.velocityEngine = tool.velocityEngine;
261            this.defaultVelocityEngine = tool.defaultVelocityEngine;
262            this.locale = tool.locale;
263            this.templateParameters =
264                tool.templateParameters != null
265                ? Collections.synchronizedMap( new HashMap<String, Object>( tool.templateParameters ) )
266                : null;
267    
268            this.templateLocation =
269                tool.templateLocation != null ? new URL( tool.templateLocation.toExternalForm() ) : null;
270    
271        }
272    
273        /**
274         * Gets the list of registered listeners.
275         * <p>This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
276         * to the returned list will be present inside the object. This is why there is no {@code set} method for the
277         * listeners property.</p>
278         *
279         * @return The list of registered listeners.
280         *
281         * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
282         */
283        public List<Listener> getListeners()
284        {
285            if ( this.listeners == null )
286            {
287                this.listeners = new CopyOnWriteArrayList<Listener>();
288            }
289    
290            return this.listeners;
291        }
292    
293        /**
294         * Gets the default log level events are logged at.
295         * <p>The default log level is controlled by system property {@code org.jomc.tools.JomcTool.defaultLogLevel} holding
296         * the log level to log events at by default. If that property is not set, the {@code WARNING} default is
297         * returned.</p>
298         *
299         * @return The log level events are logged at by default.
300         *
301         * @see #getLogLevel()
302         * @see Level#parse(java.lang.String)
303         */
304        public static Level getDefaultLogLevel()
305        {
306            if ( defaultLogLevel == null )
307            {
308                defaultLogLevel = Level.parse( System.getProperty( "org.jomc.tools.JomcTool.defaultLogLevel",
309                                                                   DEFAULT_LOG_LEVEL.getName() ) );
310    
311            }
312    
313            return defaultLogLevel;
314        }
315    
316        /**
317         * Sets the default log level events are logged at.
318         *
319         * @param value The new default level events are logged at or {@code null}.
320         *
321         * @see #getDefaultLogLevel()
322         */
323        public static void setDefaultLogLevel( final Level value )
324        {
325            defaultLogLevel = value;
326        }
327    
328        /**
329         * Gets the log level of the instance.
330         *
331         * @return The log level of the instance.
332         *
333         * @see #getDefaultLogLevel()
334         * @see #setLogLevel(java.util.logging.Level)
335         * @see #isLoggable(java.util.logging.Level)
336         */
337        public final Level getLogLevel()
338        {
339            if ( this.logLevel == null )
340            {
341                this.logLevel = getDefaultLogLevel();
342    
343                if ( this.isLoggable( Level.CONFIG ) )
344                {
345                    this.log( Level.CONFIG, getMessage( "defaultLogLevelInfo", this.logLevel.getLocalizedName() ), null );
346                }
347            }
348    
349            return this.logLevel;
350        }
351    
352        /**
353         * Sets the log level of the instance.
354         *
355         * @param value The new log level of the instance or {@code null}.
356         *
357         * @see #getLogLevel()
358         * @see #isLoggable(java.util.logging.Level)
359         */
360        public final void setLogLevel( final Level value )
361        {
362            this.logLevel = value;
363        }
364    
365        /**
366         * Checks if a message at a given level is provided to the listeners of the instance.
367         *
368         * @param level The level to test.
369         *
370         * @return {@code true}, if messages at {@code level} are provided to the listeners of the instance;
371         * {@code false}, if messages at {@code level} are not provided to the listeners of the instance.
372         *
373         * @throws NullPointerException if {@code level} is {@code null}.
374         *
375         * @see #getLogLevel()
376         * @see #setLogLevel(java.util.logging.Level)
377         * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
378         */
379        public boolean isLoggable( final Level level )
380        {
381            if ( level == null )
382            {
383                throw new NullPointerException( "level" );
384            }
385    
386            return level.intValue() >= this.getLogLevel().intValue();
387        }
388    
389        /**
390         * Gets the Java package name of a specification.
391         *
392         * @param specification The specification to get the Java package name of.
393         *
394         * @return The Java package name of {@code specification} or {@code null}.
395         *
396         * @throws NullPointerException if {@code specification} is {@code null}.
397         */
398        public String getJavaPackageName( final Specification specification )
399        {
400            if ( specification == null )
401            {
402                throw new NullPointerException( "specification" );
403            }
404    
405            return specification.getClazz() != null ? this.getJavaPackageName( specification.getClazz() ) : null;
406        }
407    
408        /**
409         * Gets the Java type name of a specification.
410         *
411         * @param specification The specification to get the Java type name of.
412         * @param qualified {@code true}, to return the fully qualified type name (with package name prepended);
413         * {@code false}, to return the short type name (without package name prepended).
414         *
415         * @return The Java type name of {@code specification} or {@code null}.
416         *
417         * @throws NullPointerException if {@code specification} is {@code null}.
418         *
419         * @see #getJavaPackageName(org.jomc.model.Specification)
420         */
421        public String getJavaTypeName( final Specification specification, final boolean qualified )
422        {
423            if ( specification == null )
424            {
425                throw new NullPointerException( "specification" );
426            }
427    
428            if ( specification.getClazz() != null )
429            {
430                final StringBuilder typeName = new StringBuilder( specification.getClazz().length() );
431                final String javaPackageName = this.getJavaPackageName( specification );
432    
433                if ( qualified && javaPackageName.length() > 0 )
434                {
435                    typeName.append( javaPackageName ).append( '.' );
436                }
437    
438                typeName.append( javaPackageName.length() > 0
439                                 ? specification.getClazz().substring( javaPackageName.length() + 1 )
440                                 : specification.getClazz() );
441    
442                return typeName.toString();
443            }
444    
445            return null;
446        }
447    
448        /**
449         * Gets the Java class path location of a specification.
450         *
451         * @param specification The specification to return the Java class path location of.
452         *
453         * @return The Java class path location of {@code specification} or {@code null}.
454         *
455         * @throws NullPointerException if {@code specification} is {@code null}.
456         *
457         * @see #getJavaTypeName(org.jomc.model.Specification, boolean)
458         */
459        public String getJavaClasspathLocation( final Specification specification )
460        {
461            if ( specification == null )
462            {
463                throw new NullPointerException( "specification" );
464            }
465    
466            return specification.getClazz() != null
467                   ? ( this.getJavaTypeName( specification, true ) ).replace( '.', '/' )
468                   : null;
469    
470        }
471    
472        /**
473         * Gets the Java package name of a specification reference.
474         *
475         * @param reference The specification reference to get the Java package name of.
476         *
477         * @return The Java package name of {@code reference} or {@code null}.
478         *
479         * @throws NullPointerException if {@code reference} is {@code null}.
480         *
481         * @see #getJavaPackageName(org.jomc.model.Specification)
482         */
483        public String getJavaPackageName( final SpecificationReference reference )
484        {
485            if ( reference == null )
486            {
487                throw new NullPointerException( "reference" );
488            }
489    
490            final Specification s = this.getModules().getSpecification( reference.getIdentifier() );
491            assert s != null : "Specification '" + reference.getIdentifier() + "' not found.";
492            return s.getClazz() != null ? this.getJavaPackageName( s ) : null;
493        }
494    
495        /**
496         * Gets the name of a Java type of a given specification reference.
497         *
498         * @param reference The specification reference to get a Java type name of.
499         * @param qualified {@code true}, to return the fully qualified type name (with package name prepended);
500         * {@code false}, to return the short type name (without package name prepended).
501         *
502         * @return The Java type name of {@code reference} or {@code null}.
503         *
504         * @throws NullPointerException if {@code reference} is {@code null}.
505         *
506         * @see #getJavaTypeName(org.jomc.model.Specification, boolean)
507         */
508        public String getJavaTypeName( final SpecificationReference reference, final boolean qualified )
509        {
510            if ( reference == null )
511            {
512                throw new NullPointerException( "reference" );
513            }
514    
515            final Specification s = this.getModules().getSpecification( reference.getIdentifier() );
516            assert s != null : "Specification '" + reference.getIdentifier() + "' not found.";
517            return s.getClazz() != null ? this.getJavaTypeName( s, qualified ) : null;
518        }
519    
520        /**
521         * Gets the Java package name of an implementation.
522         *
523         * @param implementation The implementation to get the Java package name of.
524         *
525         * @return The Java package name of {@code implementation} or {@code null}.
526         *
527         * @throws NullPointerException if {@code implementation} is {@code null}.
528         */
529        public String getJavaPackageName( final Implementation implementation )
530        {
531            if ( implementation == null )
532            {
533                throw new NullPointerException( "implementation" );
534            }
535    
536            return implementation.getClazz() != null ? this.getJavaPackageName( implementation.getClazz() ) : null;
537        }
538    
539        /**
540         * Gets the Java type name of an implementation.
541         *
542         * @param implementation The implementation to get the Java type name of.
543         * @param qualified {@code true}, to return the fully qualified type name (with package name prepended);
544         * {@code false}, to return the short type name (without package name prepended).
545         *
546         * @return The Java type name of {@code implementation} or {@code null}.
547         *
548         * @throws NullPointerException if {@code implementation} is {@code null}.
549         *
550         * @see #getJavaPackageName(org.jomc.model.Implementation)
551         */
552        public String getJavaTypeName( final Implementation implementation, final boolean qualified )
553        {
554            if ( implementation == null )
555            {
556                throw new NullPointerException( "implementation" );
557            }
558    
559            if ( implementation.getClazz() != null )
560            {
561                final StringBuilder typeName = new StringBuilder( implementation.getClazz().length() );
562                final String javaPackageName = this.getJavaPackageName( implementation );
563    
564                if ( qualified && javaPackageName.length() > 0 )
565                {
566                    typeName.append( javaPackageName ).append( '.' );
567                }
568    
569                typeName.append( javaPackageName.length() > 0
570                                 ? implementation.getClazz().substring( javaPackageName.length() + 1 )
571                                 : implementation.getClazz() );
572    
573                return typeName.toString();
574            }
575    
576            return null;
577        }
578    
579        /**
580         * Gets the Java class path location of an implementation.
581         *
582         * @param implementation The implementation to return the Java class path location of.
583         *
584         * @return The Java class path location of {@code implementation} or {@code null}.
585         *
586         * @throws NullPointerException if {@code implementation} is {@code null}.
587         */
588        public String getJavaClasspathLocation( final Implementation implementation )
589        {
590            if ( implementation == null )
591            {
592                throw new NullPointerException( "implementation" );
593            }
594    
595            return implementation.getClazz() != null
596                   ? ( this.getJavaTypeName( implementation, true ) ).replace( '.', '/' )
597                   : null;
598    
599        }
600    
601        /**
602         * Gets a list of names of all Java types an implementation implements.
603         *
604         * @param implementation The implementation to get names of all implemented Java types of.
605         * @param qualified {@code true}, to return the fully qualified type names (with package name prepended);
606         * {@code false}, to return the short type names (without package name prepended).
607         *
608         * @return An unmodifiable list of names of all Java types implemented by {@code implementation}.
609         *
610         * @throws NullPointerException if {@code implementation} is {@code null}.
611         *
612         * @deprecated As of JOMC 1.2, replaced by method {@link #getImplementedJavaTypeNames(org.jomc.model.Implementation, boolean)}.
613         * This method will be removed in version 2.0.
614         */
615        @Deprecated
616        public List<String> getJavaInterfaceNames( final Implementation implementation, final boolean qualified )
617        {
618            if ( implementation == null )
619            {
620                throw new NullPointerException( "implementation" );
621            }
622    
623            return this.getImplementedJavaTypeNames( implementation, qualified );
624        }
625    
626        /**
627         * Gets a list of names of all Java types an implementation implements.
628         *
629         * @param implementation The implementation to get names of all implemented Java types of.
630         * @param qualified {@code true}, to return the fully qualified type names (with package name prepended);
631         * {@code false}, to return the short type names (without package name prepended).
632         *
633         * @return An unmodifiable list of names of all Java types implemented by {@code implementation}.
634         *
635         * @throws NullPointerException if {@code implementation} is {@code null}.
636         *
637         * @since 1.2
638         *
639         * @see #getJavaTypeName(org.jomc.model.Specification, boolean)
640         */
641        public List<String> getImplementedJavaTypeNames( final Implementation implementation, final boolean qualified )
642        {
643            if ( implementation == null )
644            {
645                throw new NullPointerException( "implementation" );
646            }
647    
648            final Specifications specs = this.getModules().getSpecifications( implementation.getIdentifier() );
649            final List<String> col = new ArrayList<String>( specs == null ? 0 : specs.getSpecification().size() );
650    
651            if ( specs != null )
652            {
653                for ( int i = 0, s0 = specs.getSpecification().size(); i < s0; i++ )
654                {
655                    final Specification s = specs.getSpecification().get( i );
656    
657                    if ( s.getClazz() != null )
658                    {
659                        final String typeName = this.getJavaTypeName( s, qualified );
660                        if ( !col.contains( typeName ) )
661                        {
662                            col.add( typeName );
663                        }
664                    }
665                }
666            }
667    
668            return Collections.unmodifiableList( col );
669        }
670    
671        /**
672         * Gets the Java type name of an argument.
673         *
674         * @param argument The argument to get the Java type name of.
675         *
676         * @return The Java type name of {@code argument}.
677         *
678         * @throws NullPointerException if {@code argument} is {@code null}.
679         */
680        public String getJavaTypeName( final Argument argument )
681        {
682            if ( argument == null )
683            {
684                throw new NullPointerException( "argument" );
685            }
686    
687            String javaTypeName = "java.lang.String";
688    
689            if ( argument.getType() == ArgumentType.DATE || argument.getType() == ArgumentType.TIME )
690            {
691                javaTypeName = "java.util.Date";
692            }
693            else if ( argument.getType() == ArgumentType.NUMBER )
694            {
695                javaTypeName = "java.lang.Number";
696            }
697    
698            return javaTypeName;
699        }
700    
701        /**
702         * Gets a Java method parameter name of an argument.
703         *
704         * @param argument The argument to get the Java method parameter name of.
705         *
706         * @return The Java method parameter name of {@code argument}.
707         *
708         * @throws NullPointerException if {@code argument} is {@code null}.
709         *
710         * @since 1.2
711         */
712        public String getJavaMethodParameterName( final Argument argument )
713        {
714            if ( argument == null )
715            {
716                throw new NullPointerException( "argument" );
717            }
718    
719            return this.getJavaMethodParameterName( argument.getName() );
720        }
721    
722        /**
723         * Gets the Java type name of a property.
724         *
725         * @param property The property to get the Java type name of.
726         * @param boxify {@code true}, to return the name of the Java wrapper class when the type is a Java primitive type;
727         * {@code false}, to return the exact binary name (unboxed name) of the Java type.
728         *
729         * @return The Java type name of {@code property}.
730         *
731         * @throws NullPointerException if {@code property} is {@code null}.
732         */
733        public String getJavaTypeName( final Property property, final boolean boxify )
734        {
735            if ( property == null )
736            {
737                throw new NullPointerException( "property" );
738            }
739    
740            if ( property.getType() != null )
741            {
742                final String typeName = property.getType();
743    
744                if ( boxify )
745                {
746                    if ( Boolean.TYPE.getName().equals( typeName ) )
747                    {
748                        return Boolean.class.getName();
749                    }
750                    if ( Byte.TYPE.getName().equals( typeName ) )
751                    {
752                        return Byte.class.getName();
753                    }
754                    if ( Character.TYPE.getName().equals( typeName ) )
755                    {
756                        return Character.class.getName();
757                    }
758                    if ( Double.TYPE.getName().equals( typeName ) )
759                    {
760                        return Double.class.getName();
761                    }
762                    if ( Float.TYPE.getName().equals( typeName ) )
763                    {
764                        return Float.class.getName();
765                    }
766                    if ( Integer.TYPE.getName().equals( typeName ) )
767                    {
768                        return Integer.class.getName();
769                    }
770                    if ( Long.TYPE.getName().equals( typeName ) )
771                    {
772                        return Long.class.getName();
773                    }
774                    if ( Short.TYPE.getName().equals( typeName ) )
775                    {
776                        return Short.class.getName();
777                    }
778                }
779    
780                return typeName;
781            }
782    
783            return property.getAny() != null ? Object.class.getName() : String.class.getName();
784        }
785    
786        /**
787         * Gets a flag indicating the type of a given property is a Java primitive.
788         *
789         * @param property The property to query.
790         *
791         * @return {@code true}, if the Java type of {@code property} is primitive; {@code false}, if not.
792         *
793         * @throws NullPointerException if {@code property} is {@code null}.
794         *
795         * @see #getJavaTypeName(org.jomc.model.Property, boolean)
796         */
797        public boolean isJavaPrimitiveType( final Property property )
798        {
799            if ( property == null )
800            {
801                throw new NullPointerException( "property" );
802            }
803    
804            return !this.getJavaTypeName( property, false ).equals( this.getJavaTypeName( property, true ) );
805        }
806    
807        /**
808         * Gets the name of a Java getter method of a given property.
809         *
810         * @param property The property to get a Java getter method name of.
811         *
812         * @return The Java getter method name of {@code property}.
813         *
814         * @throws NullPointerException if {@code property} is {@code null}.
815         *
816         * @see #getJavaIdentifier(java.lang.String, boolean)
817         */
818        public String getJavaGetterMethodName( final Property property )
819        {
820            if ( property == null )
821            {
822                throw new NullPointerException( "property" );
823            }
824    
825            String prefix = "get";
826    
827            final String javaTypeName = this.getJavaTypeName( property, true );
828            if ( Boolean.class.getName().equals( javaTypeName ) )
829            {
830                prefix = "is";
831            }
832    
833            return prefix + this.getJavaIdentifier( property.getName(), true );
834        }
835    
836        /**
837         * Gets the name of a Java setter method of a given property.
838         *
839         * @param property The property to get a Java setter method name of.
840         *
841         * @return The Java setter method name of {@code property}.
842         *
843         * @throws NullPointerException if {@code property} is {@code null}.
844         *
845         * @see #getJavaIdentifier(java.lang.String, boolean)
846         *
847         * @since 1.2
848         */
849        public String getJavaSetterMethodName( final Property property )
850        {
851            if ( property == null )
852            {
853                throw new NullPointerException( "property" );
854            }
855    
856            return "set" + this.getJavaIdentifier( property.getName(), true );
857        }
858    
859        /**
860         * Gets a Java method parameter name of a property.
861         *
862         * @param property The property to get the Java method parameter name of.
863         *
864         * @return The Java method parameter name of {@code property}.
865         *
866         * @throws NullPointerException if {@code property} is {@code null}.
867         *
868         * @since 1.2
869         */
870        public String getJavaMethodParameterName( final Property property )
871        {
872            if ( property == null )
873            {
874                throw new NullPointerException( "property" );
875            }
876    
877            return this.getJavaMethodParameterName( property.getName() );
878        }
879    
880        /**
881         * Gets the name of a Java type of a given dependency.
882         *
883         * @param dependency The dependency to get a dependency Java type name of.
884         *
885         * @return The Java type name of {@code dependency} or {@code null}.
886         *
887         * @throws NullPointerException if {@code dependency} is {@code null}.
888         *
889         * @see #getJavaTypeName(org.jomc.model.Specification, boolean)
890         */
891        public String getJavaTypeName( final Dependency dependency )
892        {
893            if ( dependency == null )
894            {
895                throw new NullPointerException( "dependency" );
896            }
897    
898            final Specification s = this.getModules().getSpecification( dependency.getIdentifier() );
899    
900            if ( s != null && s.getClazz() != null )
901            {
902                final StringBuilder typeName = new StringBuilder( s.getClazz().length() );
903                typeName.append( this.getJavaTypeName( s, true ) );
904                if ( s.getMultiplicity() == Multiplicity.MANY && dependency.getImplementationName() == null )
905                {
906                    typeName.append( "[]" );
907                }
908    
909                return typeName.toString();
910            }
911    
912            return null;
913        }
914    
915        /**
916         * Gets the name of a Java getter method of a given dependency.
917         *
918         * @param dependency The dependency to get a Java getter method name of.
919         *
920         * @return The Java getter method name of {@code dependency}.
921         *
922         * @throws NullPointerException if {@code dependency} is {@code null}.
923         *
924         * @see #getJavaIdentifier(java.lang.String, boolean)
925         */
926        public String getJavaGetterMethodName( final Dependency dependency )
927        {
928            if ( dependency == null )
929            {
930                throw new NullPointerException( "dependency" );
931            }
932    
933            return "get" + this.getJavaIdentifier( dependency.getName(), true );
934        }
935    
936        /**
937         * Gets the name of a Java setter method of a given dependency.
938         *
939         * @param dependency The dependency to get a Java setter method name of.
940         *
941         * @return The Java setter method name of {@code dependency}.
942         *
943         * @throws NullPointerException if {@code dependency} is {@code null}.
944         *
945         * @see #getJavaIdentifier(java.lang.String, boolean)
946         *
947         * @since 1.2
948         */
949        public String getJavaSetterMethodName( final Dependency dependency )
950        {
951            if ( dependency == null )
952            {
953                throw new NullPointerException( "dependency" );
954            }
955    
956            return "set" + this.getJavaIdentifier( dependency.getName(), true );
957        }
958    
959        /**
960         * Gets a Java method parameter name of a dependency.
961         *
962         * @param dependency The dependency to get the Java method parameter name of.
963         *
964         * @return The Java method parameter name of {@code dependency}.
965         *
966         * @throws NullPointerException if {@code dependency} is {@code null}.
967         *
968         * @since 1.2
969         */
970        public String getJavaMethodParameterName( final Dependency dependency )
971        {
972            if ( dependency == null )
973            {
974                throw new NullPointerException( "dependency" );
975            }
976    
977            return this.getJavaMethodParameterName( dependency.getName() );
978        }
979    
980        /**
981         * Gets the name of a Java getter method of a given message.
982         *
983         * @param message The message to get a Java getter method name of.
984         *
985         * @return The Java getter method name of {@code message}.
986         *
987         * @throws NullPointerException if {@code message} is {@code null}.
988         *
989         * @see #getJavaIdentifier(java.lang.String, boolean)
990         */
991        public String getJavaGetterMethodName( final Message message )
992        {
993            if ( message == null )
994            {
995                throw new NullPointerException( "message" );
996            }
997    
998            return "get" + this.getJavaIdentifier( message.getName(), true );
999        }
1000    
1001        /**
1002         * Gets the name of a Java setter method of a given message.
1003         *
1004         * @param message The message to get a Java setter method name of.
1005         *
1006         * @return The Java setter method name of {@code message}.
1007         *
1008         * @throws NullPointerException if {@code message} is {@code null}.
1009         *
1010         * @see #getJavaIdentifier(java.lang.String, boolean)
1011         *
1012         * @since 1.2
1013         */
1014        public String getJavaSetterMethodName( final Message message )
1015        {
1016            if ( message == null )
1017            {
1018                throw new NullPointerException( "message" );
1019            }
1020    
1021            return "set" + this.getJavaIdentifier( message.getName(), true );
1022        }
1023    
1024        /**
1025         * Gets a Java method parameter name of a message.
1026         *
1027         * @param message The message to get the Java method parameter name of.
1028         *
1029         * @return The Java method parameter name of {@code message}.
1030         *
1031         * @throws NullPointerException if {@code message} is {@code null}.
1032         *
1033         * @since 1.2
1034         */
1035        public String getJavaMethodParameterName( final Message message )
1036        {
1037            if ( message == null )
1038            {
1039                throw new NullPointerException( "message" );
1040            }
1041    
1042            return this.getJavaMethodParameterName( message.getName() );
1043        }
1044    
1045        /**
1046         * Gets the Java modifier name of a dependency of a given implementation.
1047         *
1048         * @param implementation The implementation declaring the dependency to get a Java modifier name of.
1049         * @param dependency The dependency to get a Java modifier name of.
1050         *
1051         * @return The Java modifier name of {@code dependency} of {@code implementation}.
1052         *
1053         * @throws NullPointerException if {@code implementation} or {@code dependency} is {@code null}.
1054         */
1055        public String getJavaModifierName( final Implementation implementation, final Dependency dependency )
1056        {
1057            if ( implementation == null )
1058            {
1059                throw new NullPointerException( "implementation" );
1060            }
1061            if ( dependency == null )
1062            {
1063                throw new NullPointerException( "dependency" );
1064            }
1065    
1066            return "private";
1067        }
1068    
1069        /**
1070         * Gets the Java modifier name of a message of a given implementation.
1071         *
1072         * @param implementation The implementation declaring the message to get a Java modifier name of.
1073         * @param message The message to get a Java modifier name of.
1074         *
1075         * @return The Java modifier name of {@code message} of {@code implementation}.
1076         *
1077         * @throws NullPointerException if {@code implementation} or {@code message} is {@code null}.
1078         */
1079        public String getJavaModifierName( final Implementation implementation, final Message message )
1080        {
1081            if ( implementation == null )
1082            {
1083                throw new NullPointerException( "implementation" );
1084            }
1085            if ( message == null )
1086            {
1087                throw new NullPointerException( "message" );
1088            }
1089    
1090            return "private";
1091        }
1092    
1093        /**
1094         * Gets the Java modifier name of a property of a given implementation.
1095         *
1096         * @param implementation The implementation declaring the property to get a Java modifier name of.
1097         * @param property The property to get a Java modifier name of.
1098         *
1099         * @return The Java modifier name of {@code property} of {@code implementation}.
1100         *
1101         * @throws NullPointerException if {@code implementation} or {@code property} is {@code null}.
1102         */
1103        public String getJavaModifierName( final Implementation implementation, final Property property )
1104        {
1105            if ( implementation == null )
1106            {
1107                throw new NullPointerException( "implementation" );
1108            }
1109            if ( property == null )
1110            {
1111                throw new NullPointerException( "property" );
1112            }
1113    
1114            String modifier = "private";
1115            final Properties specified = this.getModules().getSpecifiedProperties( implementation.getIdentifier() );
1116    
1117            if ( specified != null && specified.getProperty( property.getName() ) != null )
1118            {
1119                modifier = "public";
1120            }
1121    
1122            return modifier;
1123        }
1124    
1125        /**
1126         * Formats a text to a Javadoc comment.
1127         *
1128         * @param text The text to format to a Javadoc comment.
1129         * @param indentationLevel The indentation level of the comment.
1130         * @param linePrefix The text to prepend lines with.
1131         *
1132         * @return {@code text} formatted to a Javadoc comment.
1133         *
1134         * @throws NullPointerException if {@code text} or {@code linePrefix} is {@code null}.
1135         * @throws IllegalArgumentException if {@code indentationLevel} is negative.
1136         */
1137        public String getJavadocComment( final Text text, final int indentationLevel, final String linePrefix )
1138        {
1139            if ( text == null )
1140            {
1141                throw new NullPointerException( "text" );
1142            }
1143            if ( linePrefix == null )
1144            {
1145                throw new NullPointerException( "linePrefix" );
1146            }
1147            if ( indentationLevel < 0 )
1148            {
1149                throw new IllegalArgumentException( Integer.toString( indentationLevel ) );
1150            }
1151    
1152            BufferedReader reader = null;
1153            boolean suppressExceptionOnClose = true;
1154    
1155            try
1156            {
1157                String javadoc = "";
1158    
1159                if ( text.getValue() != null )
1160                {
1161                    final String indent = this.getIndentation( indentationLevel );
1162                    reader = new BufferedReader( new StringReader( text.getValue() ) );
1163                    final StringBuilder builder = new StringBuilder( text.getValue().length() );
1164    
1165                    String line;
1166                    while ( ( line = reader.readLine() ) != null )
1167                    {
1168                        builder.append( this.getLineSeparator() ).append( indent ).append( linePrefix ).
1169                            append( line.replaceAll( "\\/\\*\\*", "/*" ).replaceAll( "\\*/", "/" ) );
1170    
1171                    }
1172    
1173                    if ( builder.length() > 0 )
1174                    {
1175                        javadoc =
1176                            builder.substring( this.getLineSeparator().length() + indent.length() + linePrefix.length() );
1177    
1178                        if ( !new MimeType( text.getType() ).match( "text/html" ) )
1179                        {
1180                            javadoc = StringEscapeUtils.escapeHtml( javadoc );
1181                        }
1182                    }
1183                }
1184    
1185                suppressExceptionOnClose = false;
1186                return javadoc;
1187            }
1188            catch ( final MimeTypeParseException e )
1189            {
1190                throw new AssertionError( e );
1191            }
1192            catch ( final IOException e )
1193            {
1194                throw new AssertionError( e );
1195            }
1196            finally
1197            {
1198                try
1199                {
1200                    if ( reader != null )
1201                    {
1202                        reader.close();
1203                    }
1204                }
1205                catch ( final IOException e )
1206                {
1207                    if ( suppressExceptionOnClose )
1208                    {
1209                        this.log( Level.SEVERE, getMessage( e ), e );
1210                    }
1211                    else
1212                    {
1213                        throw new AssertionError( e );
1214                    }
1215                }
1216            }
1217        }
1218    
1219        /**
1220         * Formats a text from a list of texts to a Javadoc comment.
1221         *
1222         * @param texts The list of texts to format to a Javadoc comment.
1223         * @param indentationLevel The indentation level of the comment.
1224         * @param linePrefix The text to prepend lines with.
1225         *
1226         * @return The text corresponding to the locale of the instance from the list of texts formatted to a Javadoc
1227         * comment.
1228         *
1229         * @throws NullPointerException if {@code texts} or {@code linePrefix} is {@code null}.
1230         * @throws IllegalArgumentException if {@code indentationLevel} is negative.
1231         *
1232         * @see #getLocale()
1233         *
1234         * @since 1.2
1235         */
1236        public String getJavadocComment( final Texts texts, final int indentationLevel, final String linePrefix )
1237        {
1238            if ( texts == null )
1239            {
1240                throw new NullPointerException( "texts" );
1241            }
1242            if ( linePrefix == null )
1243            {
1244                throw new NullPointerException( "linePrefix" );
1245            }
1246            if ( indentationLevel < 0 )
1247            {
1248                throw new IllegalArgumentException( Integer.toString( indentationLevel ) );
1249            }
1250    
1251            return this.getJavadocComment( texts.getText( this.getLocale().getLanguage() ), indentationLevel, linePrefix );
1252        }
1253    
1254        /**
1255         * Formats a string to a Java string with unicode escapes.
1256         *
1257         * @param str The string to format to a Java string or {@code null}.
1258         *
1259         * @return {@code str} formatted to a Java string or {@code null}.
1260         *
1261         * @see StringEscapeUtils#escapeJava(java.lang.String)
1262         */
1263        public String getJavaString( final String str )
1264        {
1265            return StringEscapeUtils.escapeJava( str );
1266        }
1267    
1268        /**
1269         * Formats a string to a Java identifier.
1270         *
1271         * @param str The string to format or {@code null}.
1272         * @param capitalize {@code true}, to return an identifier with the first character upper cased; {@code false}, to
1273         * return an identifier with the first character lower cased.
1274         *
1275         * @return {@code str} formatted to a Java identifier or {@code null}.
1276         *
1277         * @since 1.2
1278         */
1279        public String getJavaIdentifier( final String str, final boolean capitalize )
1280        {
1281            String identifier = null;
1282    
1283            if ( str != null )
1284            {
1285                final int len = str.length();
1286                final StringBuilder builder = new StringBuilder( len );
1287                boolean uc = capitalize;
1288    
1289                for ( int i = 0; i < len; i++ )
1290                {
1291                    final char c = str.charAt( i );
1292                    final String charString = Character.toString( c );
1293    
1294                    if ( builder.length() > 0 )
1295                    {
1296                        if ( Character.isJavaIdentifierPart( c ) )
1297                        {
1298                            builder.append( uc ? charString.toUpperCase( this.getLocale() ) : charString );
1299                            uc = false;
1300                        }
1301                        else
1302                        {
1303                            uc = true;
1304                        }
1305                    }
1306                    else
1307                    {
1308                        if ( Character.isJavaIdentifierStart( c ) )
1309                        {
1310                            builder.append( uc ? charString.toUpperCase( this.getLocale() )
1311                                            : charString.toLowerCase( this.getLocale() ) );
1312    
1313                            uc = false;
1314                        }
1315                        else
1316                        {
1317                            uc = capitalize;
1318                        }
1319                    }
1320                }
1321    
1322                identifier = builder.toString();
1323    
1324                if ( identifier.length() <= 0 && this.isLoggable( Level.WARNING ) )
1325                {
1326                    this.log( Level.WARNING, getMessage( "invalidJavaIdentifier", str ), null );
1327                }
1328            }
1329    
1330            return identifier;
1331        }
1332    
1333        /**
1334         * Formats a string to a Java method parameter name.
1335         *
1336         * @param str The string to format or {@code null}.
1337         *
1338         * @return {@code str} formatted to a Java method parameter name or {@code null}.
1339         *
1340         * @since 1.3
1341         */
1342        private String getJavaMethodParameterName( final String str )
1343        {
1344            String methodParameterName = null;
1345    
1346            if ( str != null )
1347            {
1348                final int len = str.length();
1349                final StringBuilder builder = new StringBuilder( len );
1350                boolean uc = false;
1351    
1352                for ( int i = 0; i < len; i++ )
1353                {
1354                    final char c = str.charAt( i );
1355                    final String charString = Character.toString( c );
1356    
1357                    if ( builder.length() > 0 )
1358                    {
1359                        if ( Character.isJavaIdentifierPart( c ) )
1360                        {
1361                            builder.append( uc ? charString.toUpperCase( this.getLocale() ) : charString );
1362                            uc = false;
1363                        }
1364                        else
1365                        {
1366                            uc = true;
1367                        }
1368                    }
1369                    else if ( Character.isJavaIdentifierStart( c ) )
1370                    {
1371                        builder.append( charString.toLowerCase( this.getLocale() ) );
1372                    }
1373                }
1374    
1375                methodParameterName = builder.toString();
1376    
1377                if ( methodParameterName.length() <= 0 && this.isLoggable( Level.WARNING ) )
1378                {
1379                    this.log( Level.WARNING, getMessage( "invalidJavaMethodParameterName", str ), null );
1380                }
1381    
1382                if ( this.getJavaKeywords().contains( methodParameterName ) )
1383                {
1384                    methodParameterName = "_" + methodParameterName;
1385                }
1386            }
1387    
1388            return methodParameterName;
1389        }
1390    
1391        /**
1392         * Gets a flag indicating the class of a given specification is located in the Java default package.
1393         *
1394         * @param specification The specification to query.
1395         *
1396         * @return {@code true}, if the class of {@code specification} is located in the Java default package;
1397         * {@code false}, else.
1398         *
1399         * @throws NullPointerException if {@code specification} is {@code null}.
1400         */
1401        public boolean isJavaDefaultPackage( final Specification specification )
1402        {
1403            if ( specification == null )
1404            {
1405                throw new NullPointerException( "specification" );
1406            }
1407    
1408            return specification.getClazz() != null && this.getJavaPackageName( specification ).length() == 0;
1409        }
1410    
1411        /**
1412         * Gets a flag indicating the class of a given implementation is located in the Java default package.
1413         *
1414         * @param implementation The implementation to query.
1415         *
1416         * @return {@code true}, if the class of {@code implementation} is located in the Java default package;
1417         * {@code false}, else.
1418         *
1419         * @throws NullPointerException if {@code implementation} is {@code null}.
1420         */
1421        public boolean isJavaDefaultPackage( final Implementation implementation )
1422        {
1423            if ( implementation == null )
1424            {
1425                throw new NullPointerException( "implementation" );
1426            }
1427    
1428            return implementation.getClazz() != null && this.getJavaPackageName( implementation ).length() == 0;
1429        }
1430    
1431        /**
1432         * Formats a string to a HTML string with HTML entities.
1433         *
1434         * @param str The string to format to a HTML string with HTML entities or {@code null}.
1435         *
1436         * @return {@code str} formatted to a HTML string with HTML entities or {@code null}.
1437         *
1438         * @see StringEscapeUtils#escapeHtml(java.lang.String)
1439         *
1440         * @since 1.2
1441         */
1442        public String getHtmlString( final String str )
1443        {
1444            return StringEscapeUtils.escapeHtml( str );
1445        }
1446    
1447        /**
1448         * Formats a string to a XML string with XML entities.
1449         *
1450         * @param str The string to format to a XML string with XML entities or {@code null}.
1451         *
1452         * @return {@code str} formatted to a XML string with XML entities or {@code null}.
1453         *
1454         * @see StringEscapeUtils#escapeXml(java.lang.String)
1455         *
1456         * @since 1.2
1457         */
1458        public String getXmlString( final String str )
1459        {
1460            return StringEscapeUtils.escapeXml( str );
1461        }
1462    
1463        /**
1464         * Formats a string to a JavaScript string applying JavaScript string rules.
1465         *
1466         * @param str The string to format to a JavaScript string by applying JavaScript string rules or {@code null}.
1467         *
1468         * @return {@code str} formatted to a JavaScript string with JavaScript string rules applied or {@code null}.
1469         *
1470         * @see StringEscapeUtils#escapeJavaScript(java.lang.String)
1471         *
1472         * @since 1.2
1473         */
1474        public String getJavaScriptString( final String str )
1475        {
1476            return StringEscapeUtils.escapeJavaScript( str );
1477        }
1478    
1479        /**
1480         * Formats a string to a SQL string.
1481         *
1482         * @param str The string to format to a SQL string or {@code null}.
1483         *
1484         * @return {@code str} formatted to a SQL string or {@code null}.
1485         *
1486         * @see StringEscapeUtils#escapeSql(java.lang.String)
1487         *
1488         * @since 1.2
1489         */
1490        public String getSqlString( final String str )
1491        {
1492            return StringEscapeUtils.escapeSql( str );
1493        }
1494    
1495        /**
1496         * Formats a string to a CSV string.
1497         *
1498         * @param str The string to format to a CSV string or {@code null}.
1499         *
1500         * @return {@code str} formatted to a CSV string or {@code null}.
1501         *
1502         * @see StringEscapeUtils#escapeCsv(java.lang.String)
1503         *
1504         * @since 1.2
1505         */
1506        public String getCsvString( final String str )
1507        {
1508            return StringEscapeUtils.escapeCsv( str );
1509        }
1510    
1511        /**
1512         * Formats a {@code Boolean} to a string.
1513         *
1514         * @param b The {@code Boolean} to format to a string or {@code null}.
1515         *
1516         * @return {@code b} formatted to a string.
1517         *
1518         * @see #getLocale()
1519         *
1520         * @since 1.2
1521         */
1522        public String getBooleanString( final Boolean b )
1523        {
1524            final MessageFormat messageFormat = new MessageFormat( ResourceBundle.getBundle(
1525                JomcTool.class.getName().replace( '.', '/' ), this.getLocale() ).
1526                getString( b ? "booleanStringTrue" : "booleanStringFalse" ), this.getLocale() );
1527    
1528            return messageFormat.format( null );
1529        }
1530    
1531        /**
1532         * Gets the display language of a given language code.
1533         *
1534         * @param language The language code to get the display language of.
1535         *
1536         * @return The display language of {@code language}.
1537         *
1538         * @throws NullPointerException if {@code language} is {@code null}.
1539         */
1540        public String getDisplayLanguage( final String language )
1541        {
1542            if ( language == null )
1543            {
1544                throw new NullPointerException( "language" );
1545            }
1546    
1547            final Locale l = new Locale( language );
1548            return l.getDisplayLanguage( l );
1549        }
1550    
1551        /**
1552         * Formats a calendar instance to a string.
1553         *
1554         * @param calendar The calendar to format to a string.
1555         *
1556         * @return The date of {@code calendar} formatted using a short format style pattern.
1557         *
1558         * @throws NullPointerException if {@code calendar} is {@code null}.
1559         *
1560         * @see DateFormat#SHORT
1561         */
1562        public String getShortDate( final Calendar calendar )
1563        {
1564            if ( calendar == null )
1565            {
1566                throw new NullPointerException( "calendar" );
1567            }
1568    
1569            return DateFormat.getDateInstance( DateFormat.SHORT, this.getLocale() ).format( calendar.getTime() );
1570        }
1571    
1572        /**
1573         * Formats a calendar instance to a string.
1574         *
1575         * @param calendar The calendar to format to a string.
1576         *
1577         * @return The date of {@code calendar} formatted using a medium format style pattern.
1578         *
1579         * @throws NullPointerException if {@code calendar} is {@code null}.
1580         *
1581         * @see DateFormat#MEDIUM
1582         *
1583         * @since 1.2
1584         */
1585        public String getMediumDate( final Calendar calendar )
1586        {
1587            if ( calendar == null )
1588            {
1589                throw new NullPointerException( "calendar" );
1590            }
1591    
1592            return DateFormat.getDateInstance( DateFormat.MEDIUM, this.getLocale() ).format( calendar.getTime() );
1593        }
1594    
1595        /**
1596         * Formats a calendar instance to a string.
1597         *
1598         * @param calendar The calendar to format to a string.
1599         *
1600         * @return The date of {@code calendar} formatted using a long format style pattern.
1601         *
1602         * @throws NullPointerException if {@code calendar} is {@code null}.
1603         *
1604         * @see DateFormat#LONG
1605         */
1606        public String getLongDate( final Calendar calendar )
1607        {
1608            if ( calendar == null )
1609            {
1610                throw new NullPointerException( "calendar" );
1611            }
1612    
1613            return DateFormat.getDateInstance( DateFormat.LONG, this.getLocale() ).format( calendar.getTime() );
1614        }
1615    
1616        /**
1617         * Formats a calendar instance to a string.
1618         *
1619         * @param calendar The calendar to format to a string.
1620         *
1621         * @return The date of {@code calendar} formatted using an ISO-8601 format style.
1622         *
1623         * @throws NullPointerException if {@code calendar} is {@code null}.
1624         *
1625         * @see SimpleDateFormat yyyy-DDD
1626         *
1627         * @since 1.2
1628         */
1629        public String getIsoDate( final Calendar calendar )
1630        {
1631            if ( calendar == null )
1632            {
1633                throw new NullPointerException( "calendar" );
1634            }
1635    
1636            return new SimpleDateFormat( "yyyy-DDD", this.getLocale() ).format( calendar.getTime() );
1637        }
1638    
1639        /**
1640         * Formats a calendar instance to a string.
1641         *
1642         * @param calendar The calendar to format to a string.
1643         *
1644         * @return The time of {@code calendar} formatted using a short format style pattern.
1645         *
1646         * @throws NullPointerException if {@code calendar} is {@code null}.
1647         *
1648         * @see DateFormat#SHORT
1649         */
1650        public String getShortTime( final Calendar calendar )
1651        {
1652            if ( calendar == null )
1653            {
1654                throw new NullPointerException( "calendar" );
1655            }
1656    
1657            return DateFormat.getTimeInstance( DateFormat.SHORT, this.getLocale() ).format( calendar.getTime() );
1658        }
1659    
1660        /**
1661         * Formats a calendar instance to a string.
1662         *
1663         * @param calendar The calendar to format to a string.
1664         *
1665         * @return The time of {@code calendar} formatted using a medium format style pattern.
1666         *
1667         * @throws NullPointerException if {@code calendar} is {@code null}.
1668         *
1669         * @see DateFormat#MEDIUM
1670         *
1671         * @since 1.2
1672         */
1673        public String getMediumTime( final Calendar calendar )
1674        {
1675            if ( calendar == null )
1676            {
1677                throw new NullPointerException( "calendar" );
1678            }
1679    
1680            return DateFormat.getTimeInstance( DateFormat.MEDIUM, this.getLocale() ).format( calendar.getTime() );
1681        }
1682    
1683        /**
1684         * Formats a calendar instance to a string.
1685         *
1686         * @param calendar The calendar to format to a string.
1687         *
1688         * @return The time of {@code calendar} formatted using a long format style pattern.
1689         *
1690         * @throws NullPointerException if {@code calendar} is {@code null}.
1691         *
1692         * @see DateFormat#LONG
1693         */
1694        public String getLongTime( final Calendar calendar )
1695        {
1696            if ( calendar == null )
1697            {
1698                throw new NullPointerException( "calendar" );
1699            }
1700    
1701            return DateFormat.getTimeInstance( DateFormat.LONG, this.getLocale() ).format( calendar.getTime() );
1702        }
1703    
1704        /**
1705         * Formats a calendar instance to a string.
1706         *
1707         * @param calendar The calendar to format to a string.
1708         *
1709         * @return The time of {@code calendar} formatted using an ISO-8601 format style.
1710         *
1711         * @throws NullPointerException if {@code calendar} is {@code null}.
1712         *
1713         * @see SimpleDateFormat HH:mm
1714         *
1715         * @since 1.2
1716         */
1717        public String getIsoTime( final Calendar calendar )
1718        {
1719            if ( calendar == null )
1720            {
1721                throw new NullPointerException( "calendar" );
1722            }
1723    
1724            return new SimpleDateFormat( "HH:mm", this.getLocale() ).format( calendar.getTime() );
1725        }
1726    
1727        /**
1728         * Formats a calendar instance to a string.
1729         *
1730         * @param calendar The calendar to format to a string.
1731         *
1732         * @return The date and time of {@code calendar} formatted using a short format style pattern.
1733         *
1734         * @throws NullPointerException if {@code calendar} is {@code null}.
1735         *
1736         * @see DateFormat#SHORT
1737         */
1738        public String getShortDateTime( final Calendar calendar )
1739        {
1740            if ( calendar == null )
1741            {
1742                throw new NullPointerException( "calendar" );
1743            }
1744    
1745            return DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, this.getLocale() ).
1746                format( calendar.getTime() );
1747    
1748        }
1749    
1750        /**
1751         * Formats a calendar instance to a string.
1752         *
1753         * @param calendar The calendar to format to a string.
1754         *
1755         * @return The date and time of {@code calendar} formatted using a medium format style pattern.
1756         *
1757         * @throws NullPointerException if {@code calendar} is {@code null}.
1758         *
1759         * @see DateFormat#MEDIUM
1760         *
1761         * @since 1.2
1762         */
1763        public String getMediumDateTime( final Calendar calendar )
1764        {
1765            if ( calendar == null )
1766            {
1767                throw new NullPointerException( "calendar" );
1768            }
1769    
1770            return DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.MEDIUM, this.getLocale() ).
1771                format( calendar.getTime() );
1772    
1773        }
1774    
1775        /**
1776         * Formats a calendar instance to a string.
1777         *
1778         * @param calendar The calendar to format to a string.
1779         *
1780         * @return The date and time of {@code calendar} formatted using a long format style pattern.
1781         *
1782         * @throws NullPointerException if {@code calendar} is {@code null}.
1783         *
1784         * @see DateFormat#LONG
1785         */
1786        public String getLongDateTime( final Calendar calendar )
1787        {
1788            if ( calendar == null )
1789            {
1790                throw new NullPointerException( "calendar" );
1791            }
1792    
1793            return DateFormat.getDateTimeInstance( DateFormat.LONG, DateFormat.LONG, this.getLocale() ).
1794                format( calendar.getTime() );
1795    
1796        }
1797    
1798        /**
1799         * Formats a calendar instance to a string.
1800         *
1801         * @param calendar The calendar to format to a string.
1802         *
1803         * @return The date and time of {@code calendar} formatted using a ISO-8601 format style.
1804         *
1805         * @throws NullPointerException if {@code calendar} is {@code null}.
1806         *
1807         * @see SimpleDateFormat yyyy-MM-dd'T'HH:mm:ssZ
1808         *
1809         * @since 1.2
1810         */
1811        public String getIsoDateTime( final Calendar calendar )
1812        {
1813            if ( calendar == null )
1814            {
1815                throw new NullPointerException( "calendar" );
1816            }
1817    
1818            // JDK: As of JDK 7, "yyyy-MM-dd'T'HH:mm:ssXXX".
1819            return new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssZ", this.getLocale() ).format( calendar.getTime() );
1820        }
1821    
1822        /**
1823         * Gets a string describing the range of years for given calendars.
1824         *
1825         * @param start The start of the range.
1826         * @param end The end of the range.
1827         *
1828         * @return Formatted range of the years of {@code start} and {@code end} (e.g. {@code "start - end"}).
1829         *
1830         * @throws NullPointerException if {@code start} or {@code end} is {@code null}.
1831         */
1832        public String getYears( final Calendar start, final Calendar end )
1833        {
1834            if ( start == null )
1835            {
1836                throw new NullPointerException( "start" );
1837            }
1838            if ( end == null )
1839            {
1840                throw new NullPointerException( "end" );
1841            }
1842    
1843            final Format yearFormat = new SimpleDateFormat( "yyyy", this.getLocale() );
1844            final int s = start.get( Calendar.YEAR );
1845            final int e = end.get( Calendar.YEAR );
1846            final StringBuilder years = new StringBuilder();
1847    
1848            if ( s != e )
1849            {
1850                if ( s < e )
1851                {
1852                    years.append( yearFormat.format( start.getTime() ) ).append( " - " ).
1853                        append( yearFormat.format( end.getTime() ) );
1854    
1855                }
1856                else
1857                {
1858                    years.append( yearFormat.format( end.getTime() ) ).append( " - " ).
1859                        append( yearFormat.format( start.getTime() ) );
1860    
1861                }
1862            }
1863            else
1864            {
1865                years.append( yearFormat.format( start.getTime() ) );
1866            }
1867    
1868            return years.toString();
1869        }
1870    
1871        /**
1872         * Gets the model of the instance.
1873         *
1874         * @return The model of the instance.
1875         *
1876         * @see #getModules()
1877         * @see #setModel(org.jomc.modlet.Model)
1878         */
1879        public final Model getModel()
1880        {
1881            if ( this.model == null )
1882            {
1883                this.model = new Model();
1884                this.model.setIdentifier( ModelObject.MODEL_PUBLIC_ID );
1885            }
1886    
1887            return this.model;
1888        }
1889    
1890        /**
1891         * Sets the model of the instance.
1892         *
1893         * @param value The new model of the instance or {@code null}.
1894         *
1895         * @see #getModel()
1896         */
1897        public final void setModel( final Model value )
1898        {
1899            this.model = value;
1900        }
1901    
1902        /**
1903         * Gets the modules of the instance.
1904         *
1905         * @return The modules of the instance.
1906         *
1907         * @see #getModel()
1908         * @see #setModel(org.jomc.modlet.Model)
1909         *
1910         * @deprecated As of JOMC 1.2, please use method {@link #getModel()} and {@link ModelHelper#getModules(org.jomc.modlet.Model)}.
1911         * This method will be removed in version 2.0.
1912         */
1913        @Deprecated
1914        public Modules getModules()
1915        {
1916            Modules modules = ModelHelper.getModules( this.getModel() );
1917    
1918            if ( modules == null )
1919            {
1920                modules = new Modules();
1921                ModelHelper.setModules( this.getModel(), modules );
1922            }
1923    
1924            return modules;
1925        }
1926    
1927        /**
1928         * Gets the {@code VelocityEngine} of the instance.
1929         *
1930         * @return The {@code VelocityEngine} of the instance.
1931         *
1932         * @throws IOException if initializing a new velocity engine fails.
1933         *
1934         * @see #setVelocityEngine(org.apache.velocity.app.VelocityEngine)
1935         */
1936        public final VelocityEngine getVelocityEngine() throws IOException
1937        {
1938            if ( this.velocityEngine == null )
1939            {
1940                /** {@code LogChute} logging to the listeners of the tool. */
1941                class JomcLogChute implements LogChute
1942                {
1943    
1944                    JomcLogChute()
1945                    {
1946                        super();
1947                    }
1948    
1949                    public void init( final RuntimeServices runtimeServices ) throws Exception
1950                    {
1951                    }
1952    
1953                    public void log( final int level, final String message )
1954                    {
1955                        this.log( level, message, null );
1956                    }
1957    
1958                    public void log( final int level, final String message, final Throwable throwable )
1959                    {
1960                        JomcTool.this.log( Level.FINEST, message, throwable );
1961                    }
1962    
1963                    public boolean isLevelEnabled( final int level )
1964                    {
1965                        return isLoggable( Level.FINEST );
1966                    }
1967    
1968                }
1969    
1970                final VelocityEngine engine = new VelocityEngine();
1971                engine.setProperty( RuntimeConstants.RUNTIME_REFERENCES_STRICT, Boolean.TRUE.toString() );
1972                engine.setProperty( RuntimeConstants.VM_ARGUMENTS_STRICT, Boolean.TRUE.toString() );
1973                engine.setProperty( RuntimeConstants.STRICT_MATH, Boolean.TRUE.toString() );
1974                engine.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new JomcLogChute() );
1975    
1976                engine.setProperty( RuntimeConstants.RESOURCE_LOADER, "class" );
1977                engine.setProperty( "class.resource.loader.class", ClasspathResourceLoader.class.getName() );
1978                engine.setProperty( "class.resource.loader.cache", Boolean.TRUE.toString() );
1979    
1980                if ( this.getTemplateLocation() != null )
1981                {
1982                    engine.setProperty( RuntimeConstants.RESOURCE_LOADER, "class,url" );
1983                    engine.setProperty( "url.resource.loader.class", URLResourceLoader.class.getName() );
1984                    engine.setProperty( "url.resource.loader.cache", Boolean.TRUE.toString() );
1985                    engine.setProperty( "url.resource.loader.root", this.getTemplateLocation().toExternalForm() );
1986                    engine.setProperty( "url.resource.loader.timeout", Integer.toString( 60000 ) );
1987                }
1988    
1989                this.velocityEngine = engine;
1990                this.defaultVelocityEngine = true;
1991            }
1992    
1993            return this.velocityEngine;
1994        }
1995    
1996        /**
1997         * Sets the {@code VelocityEngine} of the instance.
1998         *
1999         * @param value The new {@code VelocityEngine} of the instance or {@code null}.
2000         *
2001         * @see #getVelocityEngine()
2002         */
2003        public final void setVelocityEngine( final VelocityEngine value )
2004        {
2005            this.velocityEngine = value;
2006            this.defaultVelocityEngine = false;
2007        }
2008    
2009        /**
2010         * Gets a new velocity context used for merging templates.
2011         *
2012         * @return A new velocity context used for merging templates.
2013         *
2014         * @see #getTemplateParameters()
2015         */
2016        public VelocityContext getVelocityContext()
2017        {
2018            final Calendar now = Calendar.getInstance();
2019            final VelocityContext ctx =
2020                new VelocityContext( new HashMap<String, Object>( this.getTemplateParameters() ) );
2021    
2022            this.mergeTemplateProfileProperties( this.getTemplateProfile(), this.getLocale().getLanguage(), ctx );
2023            this.mergeTemplateProfileProperties( this.getTemplateProfile(), null, ctx );
2024            this.mergeTemplateProfileProperties( getDefaultTemplateProfile(), this.getLocale().getLanguage(), ctx );
2025            this.mergeTemplateProfileProperties( getDefaultTemplateProfile(), null, ctx );
2026    
2027            this.getModules(); // Initialization prior to cloning.
2028            final Model clonedModel = this.getModel().clone();
2029            final Modules clonedModules = ModelHelper.getModules( clonedModel );
2030            assert clonedModules != null : "Unexpected missing modules for model '" + clonedModel.getIdentifier() + "'.";
2031    
2032            ctx.put( "model", clonedModel );
2033            ctx.put( "modules", clonedModules );
2034            ctx.put( "imodel", new InheritanceModel( this.getModules() ) );
2035            ctx.put( "tool", this );
2036            ctx.put( "toolName", this.getClass().getName() );
2037            ctx.put( "toolVersion", getMessage( "projectVersion" ) );
2038            ctx.put( "toolUrl", getMessage( "projectUrl" ) );
2039            ctx.put( "calendar", now.getTime() );
2040    
2041            // JDK: As of JDK 7, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX".
2042            ctx.put( "now",
2043                     new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ", this.getLocale() ).format( now.getTime() ) );
2044    
2045            ctx.put( "year", new SimpleDateFormat( "yyyy", this.getLocale() ).format( now.getTime() ) );
2046            ctx.put( "month", new SimpleDateFormat( "MM", this.getLocale() ).format( now.getTime() ) );
2047            ctx.put( "day", new SimpleDateFormat( "dd", this.getLocale() ).format( now.getTime() ) );
2048            ctx.put( "hour", new SimpleDateFormat( "HH", this.getLocale() ).format( now.getTime() ) );
2049            ctx.put( "minute", new SimpleDateFormat( "mm", this.getLocale() ).format( now.getTime() ) );
2050            ctx.put( "second", new SimpleDateFormat( "ss", this.getLocale() ).format( now.getTime() ) );
2051            ctx.put( "timezone", new SimpleDateFormat( "Z", this.getLocale() ).format( now.getTime() ) );
2052            ctx.put( "shortDate", this.getShortDate( now ) );
2053            ctx.put( "mediumDate", this.getMediumDate( now ) );
2054            ctx.put( "longDate", this.getLongDate( now ) );
2055            ctx.put( "isoDate", this.getIsoDate( now ) );
2056            ctx.put( "shortTime", this.getShortTime( now ) );
2057            ctx.put( "mediumTime", this.getMediumTime( now ) );
2058            ctx.put( "longTime", this.getLongTime( now ) );
2059            ctx.put( "isoTime", this.getIsoTime( now ) );
2060            ctx.put( "shortDateTime", this.getShortDateTime( now ) );
2061            ctx.put( "mediumDateTime", this.getMediumDateTime( now ) );
2062            ctx.put( "longDateTime", this.getLongDateTime( now ) );
2063            ctx.put( "isoDateTime", this.getIsoDateTime( now ) );
2064    
2065            return ctx;
2066        }
2067    
2068        /**
2069         * Gets the template parameters of the instance.
2070         * <p>This accessor method returns a reference to the live map, not a snapshot. Therefore any modification you make
2071         * to the returned map will be present inside the object. This is why there is no {@code set} method for the
2072         * template parameters property.</p>
2073         *
2074         * @return The template parameters of the instance.
2075         *
2076         * @see #getVelocityContext()
2077         *
2078         * @since 1.2
2079         */
2080        public final Map<String, Object> getTemplateParameters()
2081        {
2082            if ( this.templateParameters == null )
2083            {
2084                this.templateParameters = Collections.synchronizedMap( new HashMap<String, Object>() );
2085            }
2086    
2087            return this.templateParameters;
2088        }
2089    
2090        /**
2091         * Gets the location to search for templates in addition to searching the class path.
2092         *
2093         * @return The location to search for templates in addition to searching the class path or {@code null}.
2094         *
2095         * @see #setTemplateLocation(java.net.URL)
2096         *
2097         * @since 1.2
2098         */
2099        public final URL getTemplateLocation()
2100        {
2101            return this.templateLocation;
2102        }
2103    
2104        /**
2105         * Sets the location to search for templates in addition to searching the class path.
2106         *
2107         * @param value The new location to search for templates in addition to searching the class path or {@code null}.
2108         *
2109         * @see #getTemplateLocation()
2110         *
2111         * @since 1.2
2112         */
2113        public final void setTemplateLocation( final URL value )
2114        {
2115            this.templateLocation = value;
2116            this.templateProfilePropertiesCache = null;
2117    
2118            if ( this.defaultVelocityEngine )
2119            {
2120                this.setVelocityEngine( null );
2121            }
2122        }
2123    
2124        /**
2125         * Gets the encoding to use for reading templates.
2126         *
2127         * @return The encoding to use for reading templates.
2128         *
2129         * @see #setTemplateEncoding(java.lang.String)
2130         */
2131        public final String getTemplateEncoding()
2132        {
2133            if ( this.templateEncoding == null )
2134            {
2135                this.templateEncoding = getMessage( "buildSourceEncoding" );
2136    
2137                if ( this.isLoggable( Level.CONFIG ) )
2138                {
2139                    this.log( Level.CONFIG, getMessage( "defaultTemplateEncoding", this.templateEncoding ), null );
2140                }
2141            }
2142    
2143            return this.templateEncoding;
2144        }
2145    
2146        /**
2147         * Sets the encoding to use for reading templates.
2148         *
2149         * @param value The new encoding to use for reading templates or {@code null}.
2150         *
2151         * @see #getTemplateEncoding()
2152         */
2153        public final void setTemplateEncoding( final String value )
2154        {
2155            this.templateEncoding = value;
2156        }
2157    
2158        /**
2159         * Gets the encoding to use for reading files.
2160         *
2161         * @return The encoding to use for reading files.
2162         *
2163         * @see #setInputEncoding(java.lang.String)
2164         */
2165        public final String getInputEncoding()
2166        {
2167            if ( this.inputEncoding == null )
2168            {
2169                this.inputEncoding = new InputStreamReader( new ByteArrayInputStream( NO_BYTES ) ).getEncoding();
2170    
2171                if ( this.isLoggable( Level.CONFIG ) )
2172                {
2173                    this.log( Level.CONFIG, getMessage( "defaultInputEncoding", this.inputEncoding ), null );
2174                }
2175            }
2176    
2177            return this.inputEncoding;
2178        }
2179    
2180        /**
2181         * Sets the encoding to use for reading files.
2182         *
2183         * @param value The new encoding to use for reading files or {@code null}.
2184         *
2185         * @see #getInputEncoding()
2186         */
2187        public final void setInputEncoding( final String value )
2188        {
2189            this.inputEncoding = value;
2190        }
2191    
2192        /**
2193         * Gets the encoding to use for writing files.
2194         *
2195         * @return The encoding to use for writing files.
2196         *
2197         * @see #setOutputEncoding(java.lang.String)
2198         */
2199        public final String getOutputEncoding()
2200        {
2201            if ( this.outputEncoding == null )
2202            {
2203                this.outputEncoding = new OutputStreamWriter( new ByteArrayOutputStream() ).getEncoding();
2204    
2205                if ( this.isLoggable( Level.CONFIG ) )
2206                {
2207                    this.log( Level.CONFIG, getMessage( "defaultOutputEncoding", this.outputEncoding ), null );
2208                }
2209            }
2210    
2211            return this.outputEncoding;
2212        }
2213    
2214        /**
2215         * Sets the encoding to use for writing files.
2216         *
2217         * @param value The encoding to use for writing files or {@code null}.
2218         *
2219         * @see #getOutputEncoding()
2220         */
2221        public final void setOutputEncoding( final String value )
2222        {
2223            this.outputEncoding = value;
2224        }
2225    
2226        /**
2227         * Gets the default template profile.
2228         * <p>The default template profile is controlled by system property
2229         * {@code org.jomc.tools.JomcTool.defaultTemplateProfile} holding the name of the template profile to use by
2230         * default. If that property is not set, the {@code jomc-java} default is returned.</p>
2231         *
2232         * @return The default template profile.
2233         *
2234         * @see #setDefaultTemplateProfile(java.lang.String)
2235         *
2236         * @deprecated The {@code static} modifier of this method and support to setup the default template profile using
2237         * a system property will be removed in version 2.0.
2238         */
2239        @Deprecated
2240        public static String getDefaultTemplateProfile()
2241        {
2242            if ( defaultTemplateProfile == null )
2243            {
2244                defaultTemplateProfile = System.getProperty( "org.jomc.tools.JomcTool.defaultTemplateProfile",
2245                                                             DEFAULT_TEMPLATE_PROFILE );
2246    
2247            }
2248    
2249            return defaultTemplateProfile;
2250        }
2251    
2252        /**
2253         * Sets the default template profile.
2254         *
2255         * @param value The new default template profile or {@code null}.
2256         *
2257         * @see #getDefaultTemplateProfile()
2258         *
2259         * @deprecated The {@code static} modifier of this method will be removed in version 2.0.
2260         */
2261        @Deprecated
2262        public static void setDefaultTemplateProfile( final String value )
2263        {
2264            defaultTemplateProfile = value;
2265        }
2266    
2267        /**
2268         * Gets the template profile of the instance.
2269         *
2270         * @return The template profile of the instance.
2271         *
2272         * @see #getDefaultTemplateProfile()
2273         * @see #setTemplateProfile(java.lang.String)
2274         */
2275        public final String getTemplateProfile()
2276        {
2277            if ( this.templateProfile == null )
2278            {
2279                this.templateProfile = getDefaultTemplateProfile();
2280    
2281                if ( this.isLoggable( Level.CONFIG ) )
2282                {
2283                    this.log( Level.CONFIG, getMessage( "defaultTemplateProfile", this.templateProfile ), null );
2284                }
2285            }
2286    
2287            return this.templateProfile;
2288        }
2289    
2290        /**
2291         * Sets the template profile of the instance.
2292         *
2293         * @param value The new template profile of the instance or {@code null}.
2294         *
2295         * @see #getTemplateProfile()
2296         */
2297        public final void setTemplateProfile( final String value )
2298        {
2299            this.templateProfile = value;
2300        }
2301    
2302        /**
2303         * Gets the indentation string of the instance.
2304         *
2305         * @return The indentation string of the instance.
2306         *
2307         * @see #setIndentation(java.lang.String)
2308         */
2309        public final String getIndentation()
2310        {
2311            if ( this.indentation == null )
2312            {
2313                this.indentation = "    ";
2314    
2315                if ( this.isLoggable( Level.CONFIG ) )
2316                {
2317                    this.log( Level.CONFIG, getMessage( "defaultIndentation",
2318                                                        StringEscapeUtils.escapeJava( this.indentation ) ), null );
2319    
2320                }
2321            }
2322    
2323            return this.indentation;
2324        }
2325    
2326        /**
2327         * Gets an indentation string for a given indentation level.
2328         *
2329         * @param level The indentation level to get an indentation string for.
2330         *
2331         * @return The indentation string for {@code level}.
2332         *
2333         * @throws IllegalArgumentException if {@code level} is negative.
2334         *
2335         * @see #getIndentation()
2336         */
2337        public final String getIndentation( final int level )
2338        {
2339            if ( level < 0 )
2340            {
2341                throw new IllegalArgumentException( Integer.toString( level ) );
2342            }
2343    
2344            Map<String, String> map = this.indentationCache == null ? null : this.indentationCache.get();
2345    
2346            if ( map == null )
2347            {
2348                map = new ConcurrentHashMap<String, String>( 8 );
2349                this.indentationCache = new SoftReference<Map<String, String>>( map );
2350            }
2351    
2352            final String key = this.getIndentation() + "|" + level;
2353            String idt = map.get( key );
2354    
2355            if ( idt == null )
2356            {
2357                final StringBuilder b = new StringBuilder( this.getIndentation().length() * level );
2358    
2359                for ( int i = level; i > 0; i-- )
2360                {
2361                    b.append( this.getIndentation() );
2362                }
2363    
2364                idt = b.toString();
2365                map.put( key, idt );
2366            }
2367    
2368            return idt;
2369        }
2370    
2371        /**
2372         * Sets the indentation string of the instance.
2373         *
2374         * @param value The new indentation string of the instance or {@code null}.
2375         *
2376         * @see #getIndentation()
2377         */
2378        public final void setIndentation( final String value )
2379        {
2380            this.indentation = value;
2381        }
2382    
2383        /**
2384         * Gets the line separator of the instance.
2385         *
2386         * @return The line separator of the instance.
2387         *
2388         * @see #setLineSeparator(java.lang.String)
2389         */
2390        public final String getLineSeparator()
2391        {
2392            if ( this.lineSeparator == null )
2393            {
2394                this.lineSeparator = System.getProperty( "line.separator", "\n" );
2395    
2396                if ( this.isLoggable( Level.CONFIG ) )
2397                {
2398                    this.log( Level.CONFIG, getMessage( "defaultLineSeparator",
2399                                                        StringEscapeUtils.escapeJava( this.lineSeparator ) ), null );
2400    
2401                }
2402            }
2403    
2404            return this.lineSeparator;
2405        }
2406    
2407        /**
2408         * Sets the line separator of the instance.
2409         *
2410         * @param value The new line separator of the instance or {@code null}.
2411         *
2412         * @see #getLineSeparator()
2413         */
2414        public final void setLineSeparator( final String value )
2415        {
2416            this.lineSeparator = value;
2417        }
2418    
2419        /**
2420         * Gets the locale of the instance.
2421         *
2422         * @return The locale of the instance.
2423         *
2424         * @see #setLocale(java.util.Locale)
2425         *
2426         * @since 1.2
2427         */
2428        public final Locale getLocale()
2429        {
2430            if ( this.locale == null )
2431            {
2432                this.locale = Locale.ENGLISH;
2433    
2434                if ( this.isLoggable( Level.CONFIG ) )
2435                {
2436                    this.log( Level.CONFIG, getMessage( "defaultLocale", this.locale ), null );
2437                }
2438            }
2439    
2440            return this.locale;
2441        }
2442    
2443        /**
2444         * Sets the locale of the instance.
2445         *
2446         * @param value The new locale of the instance or {@code null}.
2447         *
2448         * @see #getLocale()
2449         *
2450         * @since 1.2
2451         */
2452        public final void setLocale( final Locale value )
2453        {
2454            this.locale = value;
2455        }
2456    
2457        /**
2458         * Gets a velocity template for a given name.
2459         * <p>This method searches templates at the following locations in the shown order.
2460         * <ol>
2461         *  <li><code>org/jomc/tools/templates/{@link #getTemplateProfile() profile}/{@link #getLocale() language}/<i>templateName</i></code></li>
2462         *  <li><code>org/jomc/tools/templates/{@link #getTemplateProfile() profile}/<i>templateName</i></code></li>
2463         *  <li><code>org/jomc/tools/templates/{@link #getDefaultTemplateProfile() default profile}/{@link #getLocale() language}/<i>templateName</i></code></li>
2464         *  <li><code>org/jomc/tools/templates/{@link #getDefaultTemplateProfile() default profile}/<i>templateName</i></code></li>
2465         * </ol></p>
2466         *
2467         * @param templateName The name of the template to get.
2468         *
2469         * @return The template matching {@code templateName}.
2470         *
2471         * @throws NullPointerException if {@code templateName} is {@code null}.
2472         * @throws IOException if getting the template fails.
2473         *
2474         * @see #getLocale()
2475         * @see #getTemplateProfile()
2476         * @see #getTemplateEncoding()
2477         * @see #getVelocityEngine()
2478         */
2479        public Template getVelocityTemplate( final String templateName ) throws IOException
2480        {
2481            if ( templateName == null )
2482            {
2483                throw new NullPointerException( "templateName" );
2484            }
2485    
2486            String location = null;
2487            Template template = null;
2488            final String key = this.getLocale() + "|" + this.getTemplateProfile() + "|" + getDefaultTemplateProfile()
2489                               + "|" + templateName;
2490    
2491            Map<String, String> map = this.templateLocationsCache == null ? null : this.templateLocationsCache.get();
2492    
2493            if ( map == null )
2494            {
2495                map = Collections.synchronizedMap( new HashMap<String, String>( 32 ) );
2496                this.templateLocationsCache = new SoftReference<Map<String, String>>( map );
2497            }
2498    
2499            location = map.get( key );
2500    
2501            if ( location == null && !map.containsKey( key ) )
2502            {
2503                if ( !StringUtils.EMPTY.equals( this.getLocale().getLanguage() ) )
2504                {
2505                    location = TEMPLATE_PREFIX + this.getTemplateProfile() + "/" + this.getLocale().getLanguage() + "/"
2506                               + templateName;
2507    
2508                    template = this.findVelocityTemplate( location );
2509                }
2510    
2511                if ( template == null )
2512                {
2513                    location = TEMPLATE_PREFIX + this.getTemplateProfile() + "/" + templateName;
2514                    template = this.findVelocityTemplate( location );
2515                }
2516    
2517                if ( template == null && !StringUtils.EMPTY.equals( this.getLocale().getLanguage() ) )
2518                {
2519                    location = TEMPLATE_PREFIX + getDefaultTemplateProfile() + "/" + this.getLocale().getLanguage() + "/"
2520                               + templateName;
2521    
2522                    template = this.findVelocityTemplate( location );
2523                }
2524    
2525                if ( template == null )
2526                {
2527                    location = TEMPLATE_PREFIX + getDefaultTemplateProfile() + "/" + templateName;
2528                    template = this.findVelocityTemplate( location );
2529                }
2530    
2531                map.put( key, location );
2532            }
2533            else if ( location != null )
2534            {
2535                template = this.findVelocityTemplate( location );
2536            }
2537    
2538            if ( template == null )
2539            {
2540                throw new IOException( getMessage( "noSuchTemplate", templateName ) );
2541            }
2542    
2543            if ( this.isLoggable( Level.FINER ) )
2544            {
2545                this.log( Level.FINER, getMessage( "templateInfo", templateName, location ), null );
2546            }
2547    
2548            return template;
2549        }
2550    
2551        /**
2552         * Notifies registered listeners.
2553         *
2554         * @param level The level of the event.
2555         * @param message The message of the event or {@code null}.
2556         * @param throwable The throwable of the event or {@code null}.
2557         *
2558         * @throws NullPointerException if {@code level} is {@code null}.
2559         *
2560         * @see #getListeners()
2561         * @see #isLoggable(java.util.logging.Level)
2562         */
2563        public void log( final Level level, final String message, final Throwable throwable )
2564        {
2565            if ( level == null )
2566            {
2567                throw new NullPointerException( "level" );
2568            }
2569    
2570            if ( this.isLoggable( level ) )
2571            {
2572                for ( int i = this.getListeners().size() - 1; i >= 0; i-- )
2573                {
2574                    this.getListeners().get( i ).onLog( level, message, throwable );
2575                }
2576            }
2577        }
2578    
2579        private String getJavaPackageName( final String identifier )
2580        {
2581            if ( identifier == null )
2582            {
2583                throw new NullPointerException( "identifier" );
2584            }
2585    
2586            final int idx = identifier.lastIndexOf( '.' );
2587            return idx != -1 ? identifier.substring( 0, idx ) : "";
2588        }
2589    
2590        private Template findVelocityTemplate( final String location ) throws IOException
2591        {
2592            try
2593            {
2594                return this.getVelocityEngine().getTemplate( location, this.getTemplateEncoding() );
2595            }
2596            catch ( final ResourceNotFoundException e )
2597            {
2598                if ( this.isLoggable( Level.FINER ) )
2599                {
2600                    this.log( Level.FINER, getMessage( "templateNotFound", location ), null );
2601                }
2602    
2603                return null;
2604            }
2605            catch ( final ParseErrorException e )
2606            {
2607                String m = getMessage( e );
2608                m = m == null ? "" : " " + m;
2609    
2610                // JDK: As of JDK 6, "new IOException( message, cause )".
2611                throw (IOException) new IOException( getMessage( "invalidTemplate", location, m ) ).initCause( e );
2612            }
2613            catch ( final VelocityException e )
2614            {
2615                String m = getMessage( e );
2616                m = m == null ? "" : " " + m;
2617    
2618                // JDK: As of JDK 6, "new IOException( message, cause )".
2619                throw (IOException) new IOException( getMessage( "velocityException", location, m ) ).initCause( e );
2620            }
2621        }
2622    
2623        private java.util.Properties getTemplateProfileProperties( final String profileName, final String language )
2624        {
2625            Map<String, java.util.Properties> map = this.templateProfilePropertiesCache == null
2626                                                    ? null : this.templateProfilePropertiesCache.get();
2627    
2628            if ( map == null )
2629            {
2630                map = new ConcurrentHashMap<String, java.util.Properties>();
2631                this.templateProfilePropertiesCache = new SoftReference<Map<String, java.util.Properties>>( map );
2632            }
2633    
2634            final String key = profileName + "|" + language;
2635            java.util.Properties profileProperties = map.get( key );
2636    
2637            if ( profileProperties == null )
2638            {
2639                InputStream in = null;
2640                URL url = null;
2641                profileProperties = new java.util.Properties();
2642                map.put( key, profileProperties );
2643    
2644                final String resourceName = TEMPLATE_PREFIX + profileName + ( language == null ? "" : "/" + language )
2645                                            + "/context.properties";
2646    
2647                try
2648                {
2649                    url = this.getClass().getResource( "/" + resourceName );
2650    
2651                    if ( url != null )
2652                    {
2653                        in = url.openStream();
2654    
2655                        if ( this.isLoggable( Level.CONFIG ) )
2656                        {
2657                            this.log( Level.CONFIG, getMessage( "contextPropertiesFound", url.toExternalForm() ), null );
2658                        }
2659    
2660                        profileProperties.load( in );
2661                    }
2662                    else if ( this.getTemplateLocation() != null )
2663                    {
2664                        if ( this.isLoggable( Level.CONFIG ) )
2665                        {
2666                            this.log( Level.CONFIG, getMessage( "contextPropertiesNotFound", resourceName ), null );
2667                        }
2668    
2669                        url = new URL( this.getTemplateLocation(), resourceName );
2670                        in = url.openStream();
2671    
2672                        if ( this.isLoggable( Level.CONFIG ) )
2673                        {
2674                            this.log( Level.CONFIG, getMessage( "contextPropertiesFound", url.toExternalForm() ), null );
2675                        }
2676    
2677                        profileProperties.load( in );
2678                    }
2679                    else if ( this.isLoggable( Level.CONFIG ) )
2680                    {
2681                        this.log( Level.CONFIG, getMessage( "contextPropertiesNotFound", resourceName ), null );
2682                    }
2683                }
2684                catch ( final FileNotFoundException e )
2685                {
2686                    if ( this.isLoggable( Level.CONFIG ) )
2687                    {
2688                        this.log( Level.CONFIG, getMessage( "contextPropertiesNotFound", url.toExternalForm() ), null );
2689                    }
2690                }
2691                catch ( final IOException e )
2692                {
2693                    map.remove( key );
2694                    this.log( Level.SEVERE, getMessage( e ), e );
2695                }
2696                finally
2697                {
2698                    try
2699                    {
2700                        if ( in != null )
2701                        {
2702                            in.close();
2703                        }
2704                    }
2705                    catch ( final IOException e )
2706                    {
2707                        this.log( Level.SEVERE, getMessage( e ), e );
2708                    }
2709                }
2710            }
2711    
2712            return profileProperties;
2713        }
2714    
2715        private void mergeTemplateProfileProperties( final String profileName, final String language,
2716                                                     final VelocityContext velocityContext )
2717        {
2718            final java.util.Properties templateProfileProperties =
2719                this.getTemplateProfileProperties( profileName, language );
2720    
2721            for ( final Enumeration<?> e = templateProfileProperties.propertyNames(); e.hasMoreElements(); )
2722            {
2723                final String name = e.nextElement().toString();
2724                final String value = templateProfileProperties.getProperty( name );
2725                final String[] values = value.split( "\\|" );
2726    
2727                if ( !velocityContext.containsKey( name ) )
2728                {
2729                    try
2730                    {
2731                        if ( values.length > 1 )
2732                        {
2733                            final Class<?> valueClass = Class.forName( values[0] );
2734                            velocityContext.put( name, valueClass.getConstructor( String.class ).newInstance( values[1] ) );
2735                        }
2736                        else if ( value.contains( "|" ) )
2737                        {
2738                            velocityContext.put( name, Class.forName( values[0] ).newInstance() );
2739                        }
2740                        else
2741                        {
2742                            velocityContext.put( name, value );
2743                        }
2744                    }
2745                    catch ( final InstantiationException ex )
2746                    {
2747                        this.log( Level.SEVERE, getMessage( ex ), ex );
2748                    }
2749                    catch ( final IllegalAccessException ex )
2750                    {
2751                        this.log( Level.SEVERE, getMessage( ex ), ex );
2752                    }
2753                    catch ( final InvocationTargetException ex )
2754                    {
2755                        this.log( Level.SEVERE, getMessage( ex ), ex );
2756                    }
2757                    catch ( final NoSuchMethodException ex )
2758                    {
2759                        this.log( Level.SEVERE, getMessage( ex ), ex );
2760                    }
2761                    catch ( final ClassNotFoundException ex )
2762                    {
2763                        this.log( Level.SEVERE, getMessage( ex ), ex );
2764                    }
2765                }
2766            }
2767        }
2768    
2769        private Set<String> getJavaKeywords()
2770        {
2771            Reader in = null;
2772            Set<String> set = this.javaKeywordsCache == null ? null : this.javaKeywordsCache.get();
2773    
2774            try
2775            {
2776                if ( set == null )
2777                {
2778                    in = new InputStreamReader( this.getClass().getResourceAsStream(
2779                        "/" + this.getClass().getPackage().getName().replace( ".", "/" ) + "/JavaKeywords.txt" ), "UTF-8" );
2780    
2781                    set = new CopyOnWriteArraySet<String>( IOUtils.readLines( in ) );
2782    
2783                    this.javaKeywordsCache = new SoftReference<Set<String>>( set );
2784                }
2785            }
2786            catch ( final IOException e )
2787            {
2788                throw new IllegalStateException( getMessage( e ), e );
2789            }
2790            finally
2791            {
2792                try
2793                {
2794                    if ( in != null )
2795                    {
2796                        in.close();
2797                    }
2798                }
2799                catch ( final IOException e )
2800                {
2801                    throw new IllegalStateException( getMessage( e ), e );
2802                }
2803            }
2804    
2805            return set;
2806        }
2807    
2808        private static String getMessage( final String key, final Object... arguments )
2809        {
2810            return MessageFormat.format( ResourceBundle.getBundle(
2811                JomcTool.class.getName().replace( '.', '/' ) ).getString( key ), arguments );
2812    
2813        }
2814    
2815        private static String getMessage( final Throwable t )
2816        {
2817            return t != null ? t.getMessage() != null ? t.getMessage() : getMessage( t.getCause() ) : null;
2818        }
2819    
2820    }