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