001/*
002 *   Copyright (C) 2014 Christian Schulte <cs@schulte.it>
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: DefaultModletProcessor.java 5353 2016-09-05 04:58:44Z schulte $
029 *
030 */
031package org.jomc.modlet;
032
033import java.io.ByteArrayInputStream;
034import java.io.ByteArrayOutputStream;
035import java.io.IOException;
036import java.lang.reflect.UndeclaredThrowableException;
037import java.net.URISyntaxException;
038import java.net.URL;
039import java.text.MessageFormat;
040import java.util.Enumeration;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Locale;
044import java.util.Map;
045import java.util.Properties;
046import java.util.ResourceBundle;
047import java.util.concurrent.Callable;
048import java.util.concurrent.CancellationException;
049import java.util.concurrent.ExecutionException;
050import java.util.concurrent.Future;
051import java.util.logging.Level;
052import javax.xml.bind.JAXBContext;
053import javax.xml.bind.JAXBElement;
054import javax.xml.bind.JAXBException;
055import javax.xml.bind.util.JAXBResult;
056import javax.xml.bind.util.JAXBSource;
057import javax.xml.transform.ErrorListener;
058import javax.xml.transform.Transformer;
059import javax.xml.transform.TransformerConfigurationException;
060import javax.xml.transform.TransformerException;
061import javax.xml.transform.TransformerFactory;
062import javax.xml.transform.stream.StreamSource;
063
064/**
065 * Default {@code ModletProcessor} implementation.
066 *
067 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
068 * @version $JOMC: DefaultModletProcessor.java 5353 2016-09-05 04:58:44Z schulte $
069 * @see ModelContext#processModlets(org.jomc.modlet.Modlets)
070 * @since 1.6
071 */
072public class DefaultModletProcessor implements ModletProcessor
073{
074
075    /**
076     * Constant for the name of the model context attribute backing property {@code enabled}.
077     *
078     * @see #processModlets(org.jomc.modlet.ModelContext, org.jomc.modlet.Modlets)
079     * @see ModelContext#getAttribute(java.lang.String)
080     */
081    public static final String ENABLED_ATTRIBUTE_NAME = "org.jomc.modlet.DefaultModletProcessor.enabledAttribute";
082
083    /**
084     * Constant for the name of the system property controlling property {@code defaultEnabled}.
085     *
086     * @see #isDefaultEnabled()
087     */
088    private static final String DEFAULT_ENABLED_PROPERTY_NAME =
089        "org.jomc.modlet.DefaultModletProcessor.defaultEnabled";
090
091    /**
092     * Default value of the flag indicating the processor is enabled by default.
093     *
094     * @see #isDefaultEnabled()
095     */
096    private static final Boolean DEFAULT_ENABLED = Boolean.TRUE;
097
098    /**
099     * Flag indicating the processor is enabled by default.
100     */
101    private static volatile Boolean defaultEnabled;
102
103    /**
104     * Flag indicating the processor is enabled.
105     */
106    private volatile Boolean enabled;
107
108    /**
109     * Constant for the name of the system property controlling property {@code defaultOrdinal}.
110     *
111     * @see #getDefaultOrdinal()
112     */
113    private static final String DEFAULT_ORDINAL_PROPERTY_NAME =
114        "org.jomc.modlet.DefaultModletProcessor.defaultOrdinal";
115
116    /**
117     * Default value of the ordinal number of the processor.
118     *
119     * @see #getDefaultOrdinal()
120     */
121    private static final Integer DEFAULT_ORDINAL = 0;
122
123    /**
124     * Default ordinal number of the processor.
125     */
126    private static volatile Integer defaultOrdinal;
127
128    /**
129     * Ordinal number of the processor.
130     */
131    private volatile Integer ordinal;
132
133    /**
134     * Constant for the name of the model context attribute backing property {@code transformerLocation}.
135     *
136     * @see #processModlets(org.jomc.modlet.ModelContext, org.jomc.modlet.Modlets)
137     * @see ModelContext#getAttribute(java.lang.String)
138     * @since 1.2
139     */
140    public static final String TRANSFORMER_LOCATION_ATTRIBUTE_NAME =
141        "org.jomc.modlet.DefaultModletProcessor.transformerLocationAttribute";
142
143    /**
144     * Constant for the name of the system property controlling property {@code defaultTransformerLocation}.
145     *
146     * @see #getDefaultTransformerLocation()
147     */
148    private static final String DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME =
149        "org.jomc.modlet.DefaultModletProcessor.defaultTransformerLocation";
150
151    /**
152     * Class path location searched for transformers by default.
153     *
154     * @see #getDefaultTransformerLocation()
155     */
156    private static final String DEFAULT_TRANSFORMER_LOCATION = "META-INF/jomc-modlet.xsl";
157
158    /**
159     * Default transformer location.
160     */
161    private static volatile String defaultTransformerLocation;
162
163    /**
164     * Transformer location of the instance.
165     */
166    private volatile String transformerLocation;
167
168    /**
169     * Creates a new {@code DefaultModletProcessor} instance.
170     */
171    public DefaultModletProcessor()
172    {
173        super();
174    }
175
176    /**
177     * Gets a flag indicating the processor is enabled by default.
178     * <p>
179     * The default enabled flag is controlled by system property
180     * {@code org.jomc.modlet.DefaultModletProcessor.defaultEnabled} holding a value indicating the processor is
181     * enabled by default. If that property is not set, the {@code true} default is returned.
182     * </p>
183     *
184     * @return {@code true}, if the processor is enabled by default; {@code false}, if the processor is disabled by
185     * default.
186     *
187     * @see #isEnabled()
188     * @see #setDefaultEnabled(java.lang.Boolean)
189     */
190    public static boolean isDefaultEnabled()
191    {
192        if ( defaultEnabled == null )
193        {
194            defaultEnabled = Boolean.valueOf( System.getProperty(
195                DEFAULT_ENABLED_PROPERTY_NAME, Boolean.toString( DEFAULT_ENABLED ) ) );
196
197        }
198
199        return defaultEnabled;
200    }
201
202    /**
203     * Sets the flag indicating the processor is enabled by default.
204     *
205     * @param value The new value of the flag indicating the processor is enabled by default or {@code null}.
206     *
207     * @see #isDefaultEnabled()
208     */
209    public static void setDefaultEnabled( final Boolean value )
210    {
211        defaultEnabled = value;
212    }
213
214    /**
215     * Gets a flag indicating the processor is enabled.
216     *
217     * @return {@code true}, if the processor is enabled; {@code false}, if the processor is disabled.
218     *
219     * @see #isDefaultEnabled()
220     * @see #setEnabled(java.lang.Boolean)
221     */
222    public final boolean isEnabled()
223    {
224        if ( this.enabled == null )
225        {
226            this.enabled = isDefaultEnabled();
227        }
228
229        return this.enabled;
230    }
231
232    /**
233     * Sets the flag indicating the processor is enabled.
234     *
235     * @param value The new value of the flag indicating the processor is enabled or {@code null}.
236     *
237     * @see #isEnabled()
238     */
239    public final void setEnabled( final Boolean value )
240    {
241        this.enabled = value;
242    }
243
244    /**
245     * Gets the default ordinal number of the processor.
246     * <p>
247     * The default ordinal number is controlled by system property
248     * {@code org.jomc.modlet.DefaultModletProvider.defaultOrdinal} holding the default ordinal number of the processor.
249     * If that property is not set, the {@code 0} default is returned.
250     * </p>
251     *
252     * @return The default ordinal number of the processor.
253     *
254     * @see #setDefaultOrdinal(java.lang.Integer)
255     */
256    public static int getDefaultOrdinal()
257    {
258        if ( defaultOrdinal == null )
259        {
260            defaultOrdinal = Integer.getInteger( DEFAULT_ORDINAL_PROPERTY_NAME, DEFAULT_ORDINAL );
261        }
262
263        return defaultOrdinal;
264    }
265
266    /**
267     * Sets the default ordinal number of the processor.
268     *
269     * @param value The new default ordinal number of the processor or {@code null}.
270     *
271     * @see #getDefaultOrdinal()
272     */
273    public static void setDefaultOrdinal( final Integer value )
274    {
275        defaultOrdinal = value;
276    }
277
278    /**
279     * Gets the ordinal number of the processor.
280     *
281     * @return The ordinal number of the processor.
282     *
283     * @see #getDefaultOrdinal()
284     * @see #setOrdinal(java.lang.Integer)
285     */
286    public final int getOrdinal()
287    {
288        if ( this.ordinal == null )
289        {
290            this.ordinal = getDefaultOrdinal();
291        }
292
293        return this.ordinal;
294    }
295
296    /**
297     * Sets the ordinal number of the processor.
298     *
299     * @param value The new ordinal number of the processor or {@code null}.
300     *
301     * @see #getOrdinal()
302     */
303    public final void setOrdinal( final Integer value )
304    {
305        this.ordinal = value;
306    }
307
308    /**
309     * Gets the default location searched for transformer resources.
310     * <p>
311     * The default transformer location is controlled by system property
312     * {@code org.jomc.modlet.DefaultModletProcessor.defaultTransformerLocation} holding the location to search
313     * for transformer resources by default. If that property is not set, the {@code META-INF/jomc-modlet.xsl} default
314     * is returned.
315     * </p>
316     *
317     * @return The location searched for transformer resources by default.
318     *
319     * @see #setDefaultTransformerLocation(java.lang.String)
320     */
321    public static String getDefaultTransformerLocation()
322    {
323        if ( defaultTransformerLocation == null )
324        {
325            defaultTransformerLocation =
326                System.getProperty( DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME, DEFAULT_TRANSFORMER_LOCATION );
327
328        }
329
330        return defaultTransformerLocation;
331    }
332
333    /**
334     * Sets the default location searched for transformer resources.
335     *
336     * @param value The new default location to search for transformer resources or {@code null}.
337     *
338     * @see #getDefaultTransformerLocation()
339     */
340    public static void setDefaultTransformerLocation( final String value )
341    {
342        defaultTransformerLocation = value;
343    }
344
345    /**
346     * Gets the location searched for transformer resources.
347     *
348     * @return The location searched for transformer resources.
349     *
350     * @see #getDefaultTransformerLocation()
351     * @see #setTransformerLocation(java.lang.String)
352     */
353    public final String getTransformerLocation()
354    {
355        if ( this.transformerLocation == null )
356        {
357            this.transformerLocation = getDefaultTransformerLocation();
358        }
359
360        return this.transformerLocation;
361    }
362
363    /**
364     * Sets the location searched for transformer resources.
365     *
366     * @param value The new location to search for transformer resources or {@code null}.
367     *
368     * @see #getTransformerLocation()
369     */
370    public final void setTransformerLocation( final String value )
371    {
372        this.transformerLocation = value;
373    }
374
375    /**
376     * Searches a given context for transformers.
377     *
378     * @param context The context to search for transformers.
379     * @param location The location to search at.
380     *
381     * @return The transformers found at {@code location} in {@code context} or {@code null}, if no transformers are
382     * found.
383     *
384     * @throws NullPointerException if {@code context} or {@code location} is {@code null}.
385     * @throws ModelException if getting the transformers fails.
386     */
387    public List<Transformer> findTransformers( final ModelContext context, final String location ) throws ModelException
388    {
389        if ( context == null )
390        {
391            throw new NullPointerException( "context" );
392        }
393        if ( location == null )
394        {
395            throw new NullPointerException( "location" );
396        }
397
398        try
399        {
400            final long t0 = System.nanoTime();
401            final List<Transformer> transformers = new LinkedList<Transformer>();
402            final Enumeration<URL> transformerResourceEnumeration = context.findResources( location );
403            final ErrorListener errorListener = new ErrorListener()
404            {
405
406                public void warning( final TransformerException exception ) throws TransformerException
407                {
408                    if ( context.isLoggable( Level.WARNING ) )
409                    {
410                        context.log( Level.WARNING, getMessage( exception ), exception );
411                    }
412                }
413
414                public void error( final TransformerException exception ) throws TransformerException
415                {
416                    if ( context.isLoggable( Level.SEVERE ) )
417                    {
418                        context.log( Level.SEVERE, getMessage( exception ), exception );
419                    }
420
421                    throw exception;
422                }
423
424                public void fatalError( final TransformerException exception ) throws TransformerException
425                {
426                    if ( context.isLoggable( Level.SEVERE ) )
427                    {
428                        context.log( Level.SEVERE, getMessage( exception ), exception );
429                    }
430
431                    throw exception;
432                }
433
434            };
435
436            final Properties parameters = getTransformerParameters();
437            final ThreadLocal<TransformerFactory> threadLocalTransformerFactory = new ThreadLocal<TransformerFactory>();
438
439            class CreateTansformerTask implements Callable<Transformer>
440            {
441
442                private final URL resource;
443
444                CreateTansformerTask( final URL resource )
445                {
446                    super();
447                    this.resource = resource;
448                }
449
450                public Transformer call() throws ModelException
451                {
452                    try
453                    {
454                        TransformerFactory transformerFactory = threadLocalTransformerFactory.get();
455                        if ( transformerFactory == null )
456                        {
457                            transformerFactory = TransformerFactory.newInstance();
458                            transformerFactory.setErrorListener( errorListener );
459                            threadLocalTransformerFactory.set( transformerFactory );
460                        }
461
462                        if ( context.isLoggable( Level.FINEST ) )
463                        {
464                            context.log( Level.FINEST, getMessage( "processing", this.resource.toExternalForm() ),
465                                         null );
466
467                        }
468
469                        final Transformer transformer = transformerFactory.newTransformer(
470                            new StreamSource( this.resource.toURI().toASCIIString() ) );
471
472                        transformer.setErrorListener( errorListener );
473
474                        for ( final Map.Entry<Object, Object> e : parameters.entrySet() )
475                        {
476                            transformer.setParameter( e.getKey().toString(), e.getValue() );
477                        }
478
479                        return transformer;
480                    }
481                    catch ( final TransformerConfigurationException e )
482                    {
483                        String message = getMessage( e );
484                        if ( message == null && e.getException() != null )
485                        {
486                            message = getMessage( e.getException() );
487                        }
488
489                        throw new ModelException( message, e );
490                    }
491                    catch ( final URISyntaxException e )
492                    {
493                        throw new ModelException( getMessage( e ), e );
494                    }
495                }
496
497            }
498
499            final List<CreateTansformerTask> tasks = new LinkedList<CreateTansformerTask>();
500
501            while ( transformerResourceEnumeration.hasMoreElements() )
502            {
503                tasks.add( new CreateTansformerTask( transformerResourceEnumeration.nextElement() ) );
504            }
505
506            if ( context.getExecutorService() != null && tasks.size() > 1 )
507            {
508                for ( final Future<Transformer> task : context.getExecutorService().invokeAll( tasks ) )
509                {
510                    transformers.add( task.get() );
511                }
512            }
513            else
514            {
515                for ( final CreateTansformerTask task : tasks )
516                {
517                    transformers.add( task.call() );
518                }
519            }
520
521            if ( context.isLoggable( Level.FINE ) )
522            {
523                context.log( Level.FINE, getMessage( "contextReport", tasks.size(), location, System.nanoTime() - t0 ),
524                             null );
525
526            }
527
528            return transformers.isEmpty() ? null : transformers;
529        }
530        catch ( final CancellationException e )
531        {
532            throw new ModelException( getMessage( e ), e );
533        }
534        catch ( final InterruptedException e )
535        {
536            throw new ModelException( getMessage( e ), e );
537        }
538        catch ( final ExecutionException e )
539        {
540            if ( e.getCause() instanceof ModelException )
541            {
542                throw (ModelException) e.getCause();
543            }
544            else if ( e.getCause() instanceof RuntimeException )
545            {
546                // The fork-join framework breaks the exception handling contract of Callable by re-throwing any
547                // exception caught using a runtime exception.
548                if ( e.getCause().getCause() instanceof ModelException )
549                {
550                    throw (ModelException) e.getCause().getCause();
551                }
552                else if ( e.getCause().getCause() instanceof RuntimeException )
553                {
554                    throw (RuntimeException) e.getCause().getCause();
555                }
556                else if ( e.getCause().getCause() instanceof Error )
557                {
558                    throw (Error) e.getCause().getCause();
559                }
560                else if ( e.getCause().getCause() instanceof Exception )
561                {
562                    // Checked exception not declared to be thrown by the Callable's 'call' method.
563                    throw new UndeclaredThrowableException( e.getCause().getCause() );
564                }
565                else
566                {
567                    throw (RuntimeException) e.getCause();
568                }
569            }
570            else if ( e.getCause() instanceof Error )
571            {
572                throw (Error) e.getCause();
573            }
574            else
575            {
576                // Checked exception not declared to be thrown by the Callable's 'call' method.
577                throw new UndeclaredThrowableException( e.getCause() );
578            }
579        }
580    }
581
582    /**
583     * {@inheritDoc}
584     *
585     * @see #isEnabled()
586     * @see #getTransformerLocation()
587     * @see #findTransformers(org.jomc.modlet.ModelContext, java.lang.String)
588     * @see #ENABLED_ATTRIBUTE_NAME
589     * @see #TRANSFORMER_LOCATION_ATTRIBUTE_NAME
590     */
591    public Modlets processModlets( final ModelContext context, final Modlets modlets ) throws ModelException
592    {
593        if ( context == null )
594        {
595            throw new NullPointerException( "context" );
596        }
597        if ( modlets == null )
598        {
599            throw new NullPointerException( "modlets" );
600        }
601
602        try
603        {
604            Modlets processed = null;
605
606            boolean contextEnabled = this.isEnabled();
607            if ( DEFAULT_ENABLED == contextEnabled
608                     && context.getAttribute( ENABLED_ATTRIBUTE_NAME ) instanceof Boolean )
609            {
610                contextEnabled = (Boolean) context.getAttribute( ENABLED_ATTRIBUTE_NAME );
611            }
612
613            String contextTransformerLocation = this.getTransformerLocation();
614            if ( DEFAULT_TRANSFORMER_LOCATION.equals( contextTransformerLocation )
615                     && context.getAttribute( TRANSFORMER_LOCATION_ATTRIBUTE_NAME ) instanceof String )
616            {
617                contextTransformerLocation = (String) context.getAttribute( TRANSFORMER_LOCATION_ATTRIBUTE_NAME );
618            }
619
620            if ( contextEnabled )
621            {
622                final org.jomc.modlet.ObjectFactory objectFactory = new org.jomc.modlet.ObjectFactory();
623                final JAXBContext jaxbContext = context.createContext( ModletObject.MODEL_PUBLIC_ID );
624                final List<Transformer> transformers = this.findTransformers( context, contextTransformerLocation );
625
626                if ( transformers != null )
627                {
628                    processed = modlets.clone();
629
630                    for ( int i = 0, s0 = transformers.size(); i < s0; i++ )
631                    {
632                        final JAXBElement<Modlets> e = objectFactory.createModlets( processed );
633                        final JAXBSource source = new JAXBSource( jaxbContext, e );
634                        final JAXBResult result = new JAXBResult( jaxbContext );
635                        transformers.get( i ).transform( source, result );
636
637                        if ( result.getResult() instanceof JAXBElement<?>
638                                 && ( (JAXBElement<?>) result.getResult() ).getValue() instanceof Modlets )
639                        {
640                            processed = (Modlets) ( (JAXBElement<?>) result.getResult() ).getValue();
641                        }
642                        else
643                        {
644                            throw new ModelException( getMessage( "illegalTransformationResult" ) );
645                        }
646                    }
647                }
648            }
649            else if ( context.isLoggable( Level.FINER ) )
650            {
651                context.log( Level.FINER, getMessage( "disabled", this.getClass().getSimpleName() ), null );
652            }
653
654            return processed;
655        }
656        catch ( final TransformerException e )
657        {
658            String message = getMessage( e );
659            if ( message == null && e.getException() != null )
660            {
661                message = getMessage( e.getException() );
662            }
663
664            throw new ModelException( message, e );
665        }
666        catch ( final JAXBException e )
667        {
668            String message = getMessage( e );
669            if ( message == null && e.getLinkedException() != null )
670            {
671                message = getMessage( e.getLinkedException() );
672            }
673
674            throw new ModelException( message, e );
675        }
676    }
677
678    private static Properties getTransformerParameters() throws ModelException
679    {
680        final Properties properties = new Properties();
681
682        ByteArrayInputStream in = null;
683        ByteArrayOutputStream out = null;
684        try
685        {
686            out = new ByteArrayOutputStream();
687            System.getProperties().store( out, DefaultModletProcessor.class.getName() );
688            out.close();
689            final byte[] bytes = out.toByteArray();
690            out = null;
691
692            in = new ByteArrayInputStream( bytes );
693            properties.load( in );
694            in.close();
695            in = null;
696        }
697        catch ( final IOException e )
698        {
699            throw new ModelException( getMessage( e ), e );
700        }
701        finally
702        {
703            try
704            {
705                if ( out != null )
706                {
707                    out.close();
708                }
709            }
710            catch ( final IOException e )
711            {
712                // Suppressed.
713            }
714            finally
715            {
716                try
717                {
718                    if ( in != null )
719                    {
720                        in.close();
721                    }
722                }
723                catch ( final IOException e )
724                {
725                    // Suppressed.
726                }
727            }
728        }
729
730        return properties;
731    }
732
733    private static String getMessage( final String key, final Object... args )
734    {
735        return MessageFormat.format( ResourceBundle.getBundle( DefaultModletProcessor.class.getName(),
736                                                               Locale.getDefault() ).getString( key ), args );
737
738    }
739
740    private static String getMessage( final Throwable t )
741    {
742        return t != null
743                   ? t.getMessage() != null && t.getMessage().trim().length() > 0
744                         ? t.getMessage()
745                         : getMessage( t.getCause() )
746                   : null;
747
748    }
749
750}