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: ResourceFileProcessor.java 4670 2012-12-23 01:20:36Z schulte $
029 *
030 */
031package org.jomc.tools;
032
033import java.io.ByteArrayOutputStream;
034import java.io.Closeable;
035import java.io.File;
036import java.io.IOException;
037import java.io.RandomAccessFile;
038import java.nio.ByteBuffer;
039import java.nio.channels.FileChannel;
040import java.nio.channels.FileLock;
041import java.text.MessageFormat;
042import java.util.HashMap;
043import java.util.Locale;
044import java.util.Map;
045import java.util.Properties;
046import java.util.ResourceBundle;
047import java.util.logging.Level;
048import org.apache.velocity.VelocityContext;
049import org.jomc.model.Implementation;
050import org.jomc.model.JavaTypeName;
051import org.jomc.model.Message;
052import org.jomc.model.Messages;
053import org.jomc.model.ModelObjectException;
054import org.jomc.model.Module;
055import org.jomc.model.Specification;
056import org.jomc.model.Text;
057
058/**
059 * Processes resource files.
060 *
061 * <p><b>Use Cases:</b><br/><ul>
062 * <li>{@link #writeResourceBundleResourceFiles(File) }</li>
063 * <li>{@link #writeResourceBundleResourceFiles(Module, File) }</li>
064 * <li>{@link #writeResourceBundleResourceFiles(Specification, File) }</li>
065 * <li>{@link #writeResourceBundleResourceFiles(Implementation, File) }</li>
066 * </ul></p>
067 *
068 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
069 * @version $JOMC: ResourceFileProcessor.java 4670 2012-12-23 01:20:36Z schulte $
070 *
071 * @see #getModules()
072 */
073public class ResourceFileProcessor extends JomcTool
074{
075
076    /** The language of the default language properties file of generated resource bundle resources. */
077    private Locale resourceBundleDefaultLocale;
078
079    /** Creates a new {@code ResourceFileProcessor} instance. */
080    public ResourceFileProcessor()
081    {
082        super();
083    }
084
085    /**
086     * Creates a new {@code ResourceFileProcessor} instance taking a {@code ResourceFileProcessor} instance to
087     * initialize the instance with.
088     *
089     * @param tool The instance to initialize the new instance with.
090     *
091     * @throws NullPointerException if {@code tool} is {@code null}.
092     * @throws IOException if copying {@code tool} fails.
093     */
094    public ResourceFileProcessor( final ResourceFileProcessor tool ) throws IOException
095    {
096        super( tool );
097        this.resourceBundleDefaultLocale = tool.resourceBundleDefaultLocale;
098    }
099
100    /**
101     * Gets the language of the default language properties file of generated resource bundle resource files.
102     *
103     * @return The language of the default language properties file of generated resource bundle resource files.
104     *
105     * @see #setResourceBundleDefaultLocale(java.util.Locale)
106     */
107    public final Locale getResourceBundleDefaultLocale()
108    {
109        if ( this.resourceBundleDefaultLocale == null )
110        {
111            this.resourceBundleDefaultLocale = Locale.ENGLISH;
112
113            if ( this.isLoggable( Level.CONFIG ) )
114            {
115                this.log( Level.CONFIG, getMessage( "defaultResourceBundleDefaultLocale",
116                                                    this.resourceBundleDefaultLocale ), null );
117
118            }
119        }
120
121        return this.resourceBundleDefaultLocale;
122    }
123
124    /**
125     * Sets the language of the default language properties file of generated resource bundle resource files.
126     *
127     * @param value The language of the default language properties file of generated resource bundle resource files.
128     *
129     * @see #getResourceBundleDefaultLocale()
130     */
131    public final void setResourceBundleDefaultLocale( final Locale value )
132    {
133        this.resourceBundleDefaultLocale = value;
134    }
135
136    /**
137     * Writes resource bundle resource files of the modules of the instance to a given directory.
138     *
139     * @param resourcesDirectory The directory to write resource bundle resource files to.
140     *
141     * @throws NullPointerException if {@code resourcesDirectory} is {@code null}.
142     * @throws IOException if writing resource bundle resource files fails.
143     * @throws ModelObjectException if compiling the name of a referenced type fails.
144     *
145     * @see #writeResourceBundleResourceFiles(org.jomc.model.Module, java.io.File)
146     */
147    public void writeResourceBundleResourceFiles( final File resourcesDirectory )
148        throws IOException, ModelObjectException
149    {
150        if ( resourcesDirectory == null )
151        {
152            throw new NullPointerException( "resourcesDirectory" );
153        }
154
155        if ( this.getModules() != null )
156        {
157            for ( int i = 0, s0 = this.getModules().getModule().size(); i < s0; i++ )
158            {
159                this.writeResourceBundleResourceFiles( this.getModules().getModule().get( i ), resourcesDirectory );
160            }
161        }
162        else if ( this.isLoggable( Level.WARNING ) )
163        {
164            this.log( Level.WARNING, getMessage( "modulesNotFound", this.getModel().getIdentifier() ), null );
165        }
166    }
167
168    /**
169     * Writes resource bundle resource files of a given module from the modules of the instance to a given directory.
170     *
171     * @param module The module to process.
172     * @param resourcesDirectory The directory to write resource bundle resource files to.
173     *
174     * @throws NullPointerException if {@code module} or {@code resourcesDirectory} is {@code null}.
175     * @throws IOException if writing resource bundle resource files fails.
176     * @throws ModelObjectException if compiling the name of a referenced type fails.
177     *
178     * @see #writeResourceBundleResourceFiles(org.jomc.model.Specification, java.io.File)
179     * @see #writeResourceBundleResourceFiles(org.jomc.model.Implementation, java.io.File)
180     */
181    public void writeResourceBundleResourceFiles( final Module module, final File resourcesDirectory )
182        throws IOException, ModelObjectException
183    {
184        if ( module == null )
185        {
186            throw new NullPointerException( "module" );
187        }
188        if ( resourcesDirectory == null )
189        {
190            throw new NullPointerException( "resourcesDirectory" );
191        }
192
193        if ( this.getModules() != null && this.getModules().getModule( module.getName() ) != null )
194        {
195            if ( module.getSpecifications() != null )
196            {
197                for ( int i = 0, s0 = module.getSpecifications().getSpecification().size(); i < s0; i++ )
198                {
199                    this.writeResourceBundleResourceFiles( module.getSpecifications().getSpecification().get( i ),
200                                                           resourcesDirectory );
201
202                }
203            }
204
205            if ( module.getImplementations() != null )
206            {
207                for ( int i = 0, s0 = module.getImplementations().getImplementation().size(); i < s0; i++ )
208                {
209                    this.writeResourceBundleResourceFiles( module.getImplementations().getImplementation().get( i ),
210                                                           resourcesDirectory );
211
212                }
213            }
214        }
215        else if ( this.isLoggable( Level.WARNING ) )
216        {
217            this.log( Level.WARNING, getMessage( "moduleNotFound", module.getName() ), null );
218        }
219    }
220
221    /**
222     * Writes resource bundle resource files of a given specification from the modules of the instance to a directory.
223     *
224     * @param specification The specification to process.
225     * @param resourcesDirectory The directory to write resource bundle resource files to.
226     *
227     * @throws NullPointerException if {@code specification} or {@code resourcesDirectory} is {@code null}.
228     * @throws IOException if writing resource bundle resource files fails.
229     * @throws ModelObjectException if compiling the name of the type referenced by the specification fails.
230     *
231     * @see #getResourceBundleResources(org.jomc.model.Specification)
232     */
233    public void writeResourceBundleResourceFiles( final Specification specification, final File resourcesDirectory )
234        throws IOException, ModelObjectException
235    {
236        if ( specification == null )
237        {
238            throw new NullPointerException( "implementation" );
239        }
240        if ( resourcesDirectory == null )
241        {
242            throw new NullPointerException( "resourcesDirectory" );
243        }
244
245        if ( this.getModules() != null
246             && this.getModules().getSpecification( specification.getIdentifier() ) != null )
247        {
248            if ( specification.isClassDeclaration() )
249            {
250                if ( !resourcesDirectory.isDirectory() )
251                {
252                    throw new IOException( getMessage( "directoryNotFound", resourcesDirectory.getAbsolutePath() ) );
253                }
254
255                this.assertValidTemplates( specification );
256
257                final JavaTypeName javaTypeName = specification.getJavaTypeName();
258
259                if ( javaTypeName != null )
260                {
261                    final String bundlePath = javaTypeName.getQualifiedName().replace( '.', File.separatorChar );
262                    this.writeResourceBundleResourceFiles(
263                        this.getResourceBundleResources( specification ), resourcesDirectory, bundlePath );
264
265                }
266            }
267        }
268        else if ( this.isLoggable( Level.WARNING ) )
269        {
270            this.log( Level.WARNING, getMessage( "specificationNotFound", specification.getIdentifier() ), null );
271        }
272    }
273
274    /**
275     * Writes resource bundle resource files of a given implementation from the modules of the instance to a directory.
276     *
277     * @param implementation The implementation to process.
278     * @param resourcesDirectory The directory to write resource bundle resource files to.
279     *
280     * @throws NullPointerException if {@code implementation} or {@code resourcesDirectory} is {@code null}.
281     * @throws IOException if writing resource bundle resource files fails.
282     * @throws ModelObjectException if compiling the name of the type referenced by the implementation fails.
283     *
284     * @see #getResourceBundleResources(org.jomc.model.Implementation)
285     */
286    public void writeResourceBundleResourceFiles( final Implementation implementation, final File resourcesDirectory )
287        throws IOException, ModelObjectException
288    {
289        if ( implementation == null )
290        {
291            throw new NullPointerException( "implementation" );
292        }
293        if ( resourcesDirectory == null )
294        {
295            throw new NullPointerException( "resourcesDirectory" );
296        }
297
298        if ( this.getModules() != null
299             && this.getModules().getImplementation( implementation.getIdentifier() ) != null )
300        {
301            if ( implementation.isClassDeclaration() )
302            {
303                if ( !resourcesDirectory.isDirectory() )
304                {
305                    throw new IOException( getMessage( "directoryNotFound", resourcesDirectory.getAbsolutePath() ) );
306                }
307
308                this.assertValidTemplates( implementation );
309
310                final JavaTypeName javaTypeName = implementation.getJavaTypeName();
311
312                if ( javaTypeName != null )
313                {
314                    final String bundlePath = javaTypeName.getQualifiedName().replace( '.', File.separatorChar );
315                    this.writeResourceBundleResourceFiles(
316                        this.getResourceBundleResources( implementation ), resourcesDirectory, bundlePath );
317
318                }
319            }
320        }
321        else if ( this.isLoggable( Level.WARNING ) )
322        {
323            this.log( Level.WARNING, getMessage( "implementationNotFound", implementation.getIdentifier() ), null );
324        }
325    }
326
327    /**
328     * Gets resource bundle properties resources of a given specification.
329     *
330     * @param specification The specification to get resource bundle properties resources of.
331     *
332     * @return Resource bundle properties resources of {@code specification} or {@code null}, if no model objects are
333     * found.
334     *
335     * @throws NullPointerException if {@code specification} is {@code null}.
336     * @throws IOException if getting the resource bundle properties resources fails.
337     */
338    public Map<Locale, Properties> getResourceBundleResources( final Specification specification )
339        throws IOException
340    {
341        if ( specification == null )
342        {
343            throw new NullPointerException( "specification" );
344        }
345
346        Map<Locale, Properties> properties = null;
347
348        if ( this.getModules() != null
349             && this.getModules().getSpecification( specification.getIdentifier() ) != null )
350        {
351            properties = new HashMap<Locale, Properties>();
352        }
353        else if ( this.isLoggable( Level.WARNING ) )
354        {
355            this.log( Level.WARNING, getMessage( "specificationNotFound", specification.getIdentifier() ), null );
356        }
357
358        return properties;
359    }
360
361    /**
362     * Gets resource bundle properties resources of a given implementation.
363     *
364     * @param implementation The implementation to get resource bundle properties resources of.
365     *
366     * @return Resource bundle properties resources of {@code implementation} or {@code null}, if no model objects are
367     * found.
368     *
369     * @throws NullPointerException if {@code implementation} is {@code null}.
370     * @throws IOException if getting the resource bundle properties resources fails.
371     */
372    public Map<Locale, Properties> getResourceBundleResources( final Implementation implementation )
373        throws IOException
374    {
375        if ( implementation == null )
376        {
377            throw new NullPointerException( "implementation" );
378        }
379
380        Map<Locale, Properties> properties = null;
381
382        if ( this.getModules() != null
383             && this.getModules().getImplementation( implementation.getIdentifier() ) != null )
384        {
385            properties = new HashMap<Locale, java.util.Properties>( 10 );
386            final Messages messages = this.getModules().getMessages( implementation.getIdentifier() );
387
388            if ( messages != null )
389            {
390                for ( int i = 0, s0 = messages.getMessage().size(); i < s0; i++ )
391                {
392                    final Message message = messages.getMessage().get( i );
393
394                    if ( message.getTemplate() != null )
395                    {
396                        for ( int j = 0, s1 = message.getTemplate().getText().size(); j < s1; j++ )
397                        {
398                            final Text text = message.getTemplate().getText().get( j );
399                            final Locale locale = new Locale( text.getLanguage().toLowerCase() );
400                            Properties bundleProperties = properties.get( locale );
401
402                            if ( bundleProperties == null )
403                            {
404                                bundleProperties = new Properties();
405                                properties.put( locale, bundleProperties );
406                            }
407
408                            bundleProperties.setProperty( message.getName(), text.getValue() );
409                        }
410                    }
411                }
412            }
413        }
414        else if ( this.isLoggable( Level.WARNING ) )
415        {
416            this.log( Level.WARNING, getMessage( "implementationNotFound", implementation.getIdentifier() ), null );
417        }
418
419        return properties;
420    }
421
422    private void writeResourceBundleResourceFiles( final Map<Locale, Properties> resources,
423                                                   final File resourcesDirectory, final String bundlePath )
424        throws IOException
425    {
426        if ( resources == null )
427        {
428            throw new NullPointerException( "resources" );
429        }
430        if ( resourcesDirectory == null )
431        {
432            throw new NullPointerException( "resourcesDirectory" );
433        }
434        if ( bundlePath == null )
435        {
436            throw new NullPointerException( "bundlePath" );
437        }
438
439        Properties defProperties = null;
440        Properties fallbackProperties = null;
441
442        final VelocityContext ctx = this.getVelocityContext();
443        final String toolName = ctx.get( "toolName" ).toString();
444        final String toolVersion = ctx.get( "toolVersion" ).toString();
445        final String toolUrl = ctx.get( "toolUrl" ).toString();
446
447        for ( Map.Entry<Locale, Properties> e : resources.entrySet() )
448        {
449            final String language = e.getKey().getLanguage().toLowerCase();
450            final Properties p = e.getValue();
451            final File file = new File( resourcesDirectory, bundlePath + "_" + language + ".properties" );
452
453            if ( this.getResourceBundleDefaultLocale().getLanguage().equalsIgnoreCase( language ) )
454            {
455                defProperties = p;
456            }
457
458            fallbackProperties = p;
459
460            if ( !file.getParentFile().exists() && !file.getParentFile().mkdirs() )
461            {
462                throw new IOException( getMessage( "failedCreatingDirectory",
463                                                   file.getParentFile().getAbsolutePath() ) );
464
465            }
466
467            if ( this.isLoggable( Level.INFO ) )
468            {
469                this.log( Level.INFO, getMessage( "writing", file.getCanonicalPath() ), null );
470            }
471
472            this.writePropertiesFile( p, toolName + ' ' + toolVersion + " - See " + toolUrl, file );
473        }
474
475        if ( defProperties == null )
476        {
477            defProperties = fallbackProperties;
478        }
479
480        if ( defProperties != null )
481        {
482            final File file = new File( resourcesDirectory, bundlePath + ".properties" );
483
484            if ( !file.getParentFile().exists() && !file.getParentFile().mkdirs() )
485            {
486                throw new IOException( getMessage( "failedCreatingDirectory",
487                                                   file.getParentFile().getAbsolutePath() ) );
488
489            }
490
491            if ( this.isLoggable( Level.INFO ) )
492            {
493                this.log( Level.INFO, getMessage( "writing", file.getCanonicalPath() ), null );
494            }
495
496            this.writePropertiesFile( defProperties, toolName + ' ' + toolVersion + " - See " + toolUrl, file );
497        }
498    }
499
500    private void assertValidTemplates( final Specification specification )
501    {
502        if ( specification == null )
503        {
504            throw new NullPointerException( "specification" );
505        }
506    }
507
508    private void assertValidTemplates( final Implementation implementation )
509    {
510        if ( implementation == null )
511        {
512            throw new NullPointerException( "implementation" );
513        }
514
515        final Messages messages = this.getModules().getMessages( implementation.getIdentifier() );
516
517        if ( messages != null )
518        {
519            for ( int i = messages.getMessage().size() - 1; i >= 0; i-- )
520            {
521                final Message m = messages.getMessage().get( i );
522
523                if ( m.getTemplate() != null )
524                {
525                    for ( int j = m.getTemplate().getText().size() - 1; j >= 0; j-- )
526                    {
527                        new MessageFormat( m.getTemplate().getText().get( j ).getValue() );
528                    }
529                }
530            }
531        }
532    }
533
534    private void writePropertiesFile( final Properties properties, final String comments, final File propertiesFile )
535        throws IOException
536    {
537        RandomAccessFile randomAccessFile = null;
538        FileChannel fileChannel = null;
539        FileLock fileLock = null;
540        boolean suppressExceptionOnClose = true;
541
542        final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
543        properties.store( byteStream, comments );
544        byteStream.close();
545
546        final byte[] bytes = byteStream.toByteArray();
547
548        try
549        {
550            randomAccessFile = new RandomAccessFile( propertiesFile, "rw" );
551            fileChannel = randomAccessFile.getChannel();
552            fileLock = fileChannel.lock();
553            fileChannel.truncate( bytes.length );
554            fileChannel.position( 0L );
555            fileChannel.write( ByteBuffer.wrap( bytes ) );
556            fileChannel.force( true );
557            suppressExceptionOnClose = false;
558        }
559        finally
560        {
561            this.releaseAndClose( fileLock, fileChannel, randomAccessFile, suppressExceptionOnClose );
562        }
563    }
564
565    private void releaseAndClose( final FileLock fileLock, final FileChannel fileChannel,
566                                  final Closeable closeable, final boolean suppressExceptions )
567        throws IOException
568    {
569        try
570        {
571            if ( fileLock != null )
572            {
573                fileLock.release();
574            }
575        }
576        catch ( final IOException e )
577        {
578            if ( suppressExceptions )
579            {
580                this.log( Level.SEVERE, null, e );
581            }
582            else
583            {
584                throw e;
585            }
586        }
587        finally
588        {
589            try
590            {
591                if ( fileChannel != null )
592                {
593                    fileChannel.close();
594                }
595            }
596            catch ( final IOException e )
597            {
598                if ( suppressExceptions )
599                {
600                    this.log( Level.SEVERE, null, e );
601                }
602                else
603                {
604                    throw e;
605                }
606            }
607            finally
608            {
609                try
610                {
611                    if ( closeable != null )
612                    {
613                        closeable.close();
614                    }
615                }
616                catch ( final IOException e )
617                {
618                    if ( suppressExceptions )
619                    {
620                        this.log( Level.SEVERE, null, e );
621                    }
622                    else
623                    {
624                        throw e;
625                    }
626                }
627            }
628        }
629    }
630
631    private static String getMessage( final String key, final Object... arguments )
632    {
633        if ( key == null )
634        {
635            throw new NullPointerException( "key" );
636        }
637
638        return MessageFormat.format( ResourceBundle.getBundle(
639            ResourceFileProcessor.class.getName().replace( '.', '/' ) ).getString( key ), arguments );
640
641    }
642
643}