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