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