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