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