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