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