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