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: DefaultModelProcessor.java 4760 2013-04-08 17:56:26Z schulte $
029 *
030 */
031package org.jomc.model.modlet;
032
033import java.net.URISyntaxException;
034import java.net.URL;
035import java.text.MessageFormat;
036import java.util.Enumeration;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Locale;
040import java.util.Map;
041import java.util.ResourceBundle;
042import java.util.logging.Level;
043import javax.xml.bind.JAXBContext;
044import javax.xml.bind.JAXBElement;
045import javax.xml.bind.JAXBException;
046import javax.xml.bind.util.JAXBResult;
047import javax.xml.bind.util.JAXBSource;
048import javax.xml.transform.ErrorListener;
049import javax.xml.transform.Transformer;
050import javax.xml.transform.TransformerConfigurationException;
051import javax.xml.transform.TransformerException;
052import javax.xml.transform.TransformerFactory;
053import javax.xml.transform.stream.StreamSource;
054import org.jomc.modlet.Model;
055import org.jomc.modlet.ModelContext;
056import org.jomc.modlet.ModelException;
057import org.jomc.modlet.ModelProcessor;
058
059/**
060 * Default object management and configuration {@code ModelProcessor} implementation.
061 *
062 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
063 * @version $JOMC: DefaultModelProcessor.java 4760 2013-04-08 17:56:26Z schulte $
064 * @see ModelContext#processModel(org.jomc.modlet.Model)
065 */
066public class DefaultModelProcessor implements ModelProcessor
067{
068
069    /**
070     * Constant for the name of the model context attribute backing property {@code enabled}.
071     * @see #processModel(org.jomc.modlet.ModelContext, org.jomc.modlet.Model)
072     * @see ModelContext#getAttribute(java.lang.String)
073     * @since 1.2
074     */
075    public static final String ENABLED_ATTRIBUTE_NAME = "org.jomc.model.modlet.DefaultModelProcessor.enabledAttribute";
076
077    /**
078     * Constant for the name of the system property controlling property {@code defaultEnabled}.
079     * @see #isDefaultEnabled()
080     */
081    private static final String DEFAULT_ENABLED_PROPERTY_NAME =
082        "org.jomc.model.modlet.DefaultModelProcessor.defaultEnabled";
083
084    /**
085     * Constant for the name of the deprecated system property controlling property {@code defaultEnabled}.
086     * @see #isDefaultEnabled()
087     */
088    private static final String DEPRECATED_DEFAULT_ENABLED_PROPERTY_NAME =
089        "org.jomc.model.DefaultModelProcessor.defaultEnabled";
090
091    /**
092     * Default value of the flag indicating the processor is enabled by default.
093     * @see #isDefaultEnabled()
094     * @since 1.2
095     */
096    private static final Boolean DEFAULT_ENABLED = Boolean.TRUE;
097
098    /** Flag indicating the processor is enabled by default. */
099    private static volatile Boolean defaultEnabled;
100
101    /** Flag indicating the processor is enabled. */
102    private Boolean enabled;
103
104    /**
105     * Constant for the name of the model context attribute backing property {@code transformerLocation}.
106     * @see #processModel(org.jomc.modlet.ModelContext, org.jomc.modlet.Model)
107     * @see ModelContext#getAttribute(java.lang.String)
108     * @since 1.2
109     */
110    public static final String TRANSFORMER_LOCATION_ATTRIBUTE_NAME =
111        "org.jomc.model.modlet.DefaultModelProcessor.transformerLocationAttribute";
112
113    /**
114     * Constant for the name of the system property controlling property {@code defaultTransformerLocation}.
115     * @see #getDefaultTransformerLocation()
116     */
117    private static final String DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME =
118        "org.jomc.model.modlet.DefaultModelProcessor.defaultTransformerLocation";
119
120    /**
121     * Constant for the name of the deprecated system property controlling property {@code defaultTransformerLocation}.
122     * @see #getDefaultTransformerLocation()
123     */
124    private static final String DEPRECATED_DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME =
125        "org.jomc.model.DefaultModelProcessor.defaultTransformerLocation";
126
127    /**
128     * Class path location searched for transformers by default.
129     * @see #getDefaultTransformerLocation()
130     */
131    private static final String DEFAULT_TRANSFORMER_LOCATION = "META-INF/jomc.xsl";
132
133    /** Default transformer location. */
134    private static volatile String defaultTransformerLocation;
135
136    /** Transformer location of the instance. */
137    private String transformerLocation;
138
139    /** Creates a new {@code DefaultModelProcessor} instance. */
140    public DefaultModelProcessor()
141    {
142        super();
143    }
144
145    /**
146     * Gets a flag indicating the processor is enabled by default.
147     * <p>The default enabled flag is controlled by system property
148     * {@code org.jomc.model.modlet.DefaultModelProcessor.defaultEnabled} holding a value indicating the processor is
149     * enabled by default. If that property is not set, the {@code true} default is returned.</p>
150     *
151     * @return {@code true}, if the processor is enabled by default; {@code false}, if the processor is disabled by
152     * default.
153     *
154     * @see #setDefaultEnabled(java.lang.Boolean)
155     */
156    public static boolean isDefaultEnabled()
157    {
158        if ( defaultEnabled == null )
159        {
160            defaultEnabled =
161                Boolean.valueOf( System.getProperty( DEFAULT_ENABLED_PROPERTY_NAME,
162                                                     System.getProperty( DEPRECATED_DEFAULT_ENABLED_PROPERTY_NAME,
163                                                                         Boolean.toString( DEFAULT_ENABLED ) ) ) );
164
165        }
166
167        return defaultEnabled;
168    }
169
170    /**
171     * Sets the flag indicating the processor is enabled by default.
172     *
173     * @param value The new value of the flag indicating the processor is enabled by default or {@code null}.
174     *
175     * @see #isDefaultEnabled()
176     */
177    public static void setDefaultEnabled( final Boolean value )
178    {
179        defaultEnabled = value;
180    }
181
182    /**
183     * Gets a flag indicating the processor is enabled.
184     *
185     * @return {@code true}, if the processor is enabled; {@code false}, if the processor is disabled.
186     *
187     * @see #isDefaultEnabled()
188     * @see #setEnabled(java.lang.Boolean)
189     */
190    public final boolean isEnabled()
191    {
192        if ( this.enabled == null )
193        {
194            this.enabled = isDefaultEnabled();
195        }
196
197        return this.enabled;
198    }
199
200    /**
201     * Sets the flag indicating the processor is enabled.
202     *
203     * @param value The new value of the flag indicating the processor is enabled or {@code null}.
204     *
205     * @see #isEnabled()
206     */
207    public final void setEnabled( final Boolean value )
208    {
209        this.enabled = value;
210    }
211
212    /**
213     * Gets the default location searched for transformer resources.
214     * <p>The default transformer location is controlled by system property
215     * {@code org.jomc.model.modlet.DefaultModelProcessor.defaultTransformerLocation} holding the location to search for
216     * transformer resources by default. If that property is not set, the {@code META-INF/jomc.xsl} default is
217     * returned.</p>
218     *
219     * @return The location searched for transformer resources by default.
220     *
221     * @see #setDefaultTransformerLocation(java.lang.String)
222     */
223    public static String getDefaultTransformerLocation()
224    {
225        if ( defaultTransformerLocation == null )
226        {
227            defaultTransformerLocation =
228                System.getProperty( DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME,
229                                    System.getProperty( DEPRECATED_DEFAULT_TRANSFORMER_LOCATION_PROPERTY_NAME,
230                                                        DEFAULT_TRANSFORMER_LOCATION ) );
231
232        }
233
234        return defaultTransformerLocation;
235    }
236
237    /**
238     * Sets the default location searched for transformer resources.
239     *
240     * @param value The new default location to search for transformer resources or {@code null}.
241     *
242     * @see #getDefaultTransformerLocation()
243     */
244    public static void setDefaultTransformerLocation( final String value )
245    {
246        defaultTransformerLocation = value;
247    }
248
249    /**
250     * Gets the location searched for transformer resources.
251     *
252     * @return The location searched for transformer resources.
253     *
254     * @see #getDefaultTransformerLocation()
255     * @see #setTransformerLocation(java.lang.String)
256     */
257    public final String getTransformerLocation()
258    {
259        if ( this.transformerLocation == null )
260        {
261            this.transformerLocation = getDefaultTransformerLocation();
262        }
263
264        return this.transformerLocation;
265    }
266
267    /**
268     * Sets the location searched for transformer resources.
269     *
270     * @param value The new location to search for transformer resources or {@code null}.
271     *
272     * @see #getTransformerLocation()
273     */
274    public final void setTransformerLocation( final String value )
275    {
276        this.transformerLocation = value;
277    }
278
279    /**
280     * Searches a given context for transformers.
281     *
282     * @param context The context to search for transformers.
283     * @param location The location to search at.
284     *
285     * @return The transformers found at {@code location} in {@code context} or {@code null}, if no transformers are
286     * found.
287     *
288     * @throws NullPointerException if {@code context} or {@code location} is {@code null}.
289     * @throws ModelException if getting the transformers fails.
290     */
291    public List<Transformer> findTransformers( final ModelContext context, final String location ) throws ModelException
292    {
293        if ( context == null )
294        {
295            throw new NullPointerException( "context" );
296        }
297        if ( location == null )
298        {
299            throw new NullPointerException( "location" );
300        }
301
302        try
303        {
304            final long t0 = System.currentTimeMillis();
305            final List<Transformer> transformers = new LinkedList<Transformer>();
306            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
307            final Enumeration<URL> resources = context.findResources( location );
308            final ErrorListener errorListener = new ErrorListener()
309            {
310
311                public void warning( final TransformerException exception ) throws TransformerException
312                {
313                    if ( context.isLoggable( Level.WARNING ) )
314                    {
315                        context.log( Level.WARNING, getMessage( exception ), exception );
316                    }
317                }
318
319                public void error( final TransformerException exception ) throws TransformerException
320                {
321                    if ( context.isLoggable( Level.SEVERE ) )
322                    {
323                        context.log( Level.SEVERE, getMessage( exception ), exception );
324                    }
325
326                    throw exception;
327                }
328
329                public void fatalError( final TransformerException exception ) throws TransformerException
330                {
331                    if ( context.isLoggable( Level.SEVERE ) )
332                    {
333                        context.log( Level.SEVERE, getMessage( exception ), exception );
334                    }
335
336                    throw exception;
337                }
338
339            };
340
341            transformerFactory.setErrorListener( errorListener );
342
343            int count = 0;
344            while ( resources.hasMoreElements() )
345            {
346                count++;
347                final URL url = resources.nextElement();
348
349                if ( context.isLoggable( Level.FINEST ) )
350                {
351                    context.log( Level.FINEST, getMessage( "processing", url.toExternalForm() ), null );
352                }
353
354                final Transformer transformer =
355                    transformerFactory.newTransformer( new StreamSource( url.toURI().toASCIIString() ) );
356
357                transformer.setErrorListener( errorListener );
358
359                for ( Map.Entry<Object, Object> e : System.getProperties().entrySet() )
360                {
361                    transformer.setParameter( e.getKey().toString(), e.getValue() );
362                }
363
364                transformers.add( transformer );
365            }
366
367            if ( context.isLoggable( Level.FINE ) )
368            {
369                context.log( Level.FINE, getMessage( "contextReport", count, location,
370                                                     Long.valueOf( System.currentTimeMillis() - t0 ) ), null );
371
372            }
373
374            return transformers.isEmpty() ? null : transformers;
375        }
376        catch ( final URISyntaxException e )
377        {
378            throw new ModelException( getMessage( e ), e );
379        }
380        catch ( final TransformerConfigurationException e )
381        {
382            String message = getMessage( e );
383            if ( message == null && e.getException() != null )
384            {
385                message = getMessage( e.getException() );
386            }
387
388            throw new ModelException( message, e );
389        }
390    }
391
392    /**
393     * {@inheritDoc}
394     *
395     * @see #isEnabled()
396     * @see #getTransformerLocation()
397     * @see #findTransformers(org.jomc.modlet.ModelContext, java.lang.String)
398     * @see #ENABLED_ATTRIBUTE_NAME
399     * @see #TRANSFORMER_LOCATION_ATTRIBUTE_NAME
400     */
401    public Model processModel( final ModelContext context, final Model model ) throws ModelException
402    {
403        if ( context == null )
404        {
405            throw new NullPointerException( "context" );
406        }
407        if ( model == null )
408        {
409            throw new NullPointerException( "model" );
410        }
411
412        try
413        {
414            Model processed = model;
415
416            boolean contextEnabled = this.isEnabled();
417            if ( DEFAULT_ENABLED == contextEnabled
418                 && context.getAttribute( ENABLED_ATTRIBUTE_NAME ) instanceof Boolean )
419            {
420                contextEnabled = (Boolean) context.getAttribute( ENABLED_ATTRIBUTE_NAME );
421            }
422
423            String contextTransformerLocation = this.getTransformerLocation();
424            if ( DEFAULT_TRANSFORMER_LOCATION.equals( contextTransformerLocation )
425                 && context.getAttribute( TRANSFORMER_LOCATION_ATTRIBUTE_NAME ) instanceof String )
426            {
427                contextTransformerLocation = (String) context.getAttribute( TRANSFORMER_LOCATION_ATTRIBUTE_NAME );
428            }
429
430            if ( contextEnabled )
431            {
432                final org.jomc.modlet.ObjectFactory objectFactory = new org.jomc.modlet.ObjectFactory();
433                final JAXBContext jaxbContext = context.createContext( model.getIdentifier() );
434                final List<Transformer> transformers = this.findTransformers( context, contextTransformerLocation );
435                processed = model.clone();
436
437                if ( transformers != null )
438                {
439                    for ( int i = 0, s0 = transformers.size(); i < s0; i++ )
440                    {
441                        final JAXBElement<Model> e = objectFactory.createModel( processed );
442                        final JAXBSource source = new JAXBSource( jaxbContext, e );
443                        final JAXBResult result = new JAXBResult( jaxbContext );
444                        transformers.get( i ).transform( source, result );
445
446                        if ( result.getResult() instanceof JAXBElement<?>
447                             && ( (JAXBElement<?>) result.getResult() ).getValue() instanceof Model )
448                        {
449                            processed = (Model) ( (JAXBElement<?>) result.getResult() ).getValue();
450                        }
451                        else
452                        {
453                            throw new ModelException( getMessage(
454                                "illegalTransformationResult", model.getIdentifier() ) );
455
456                        }
457                    }
458                }
459            }
460            else if ( context.isLoggable( Level.FINER ) )
461            {
462                context.log( Level.FINER, getMessage( "disabled", this.getClass().getSimpleName(),
463                                                      model.getIdentifier() ), null );
464
465            }
466
467            return processed;
468        }
469        catch ( final TransformerException e )
470        {
471            String message = getMessage( e );
472            if ( message == null && e.getException() != null )
473            {
474                message = getMessage( e.getException() );
475            }
476
477            throw new ModelException( message, e );
478        }
479        catch ( final JAXBException e )
480        {
481            String message = getMessage( e );
482            if ( message == null && e.getLinkedException() != null )
483            {
484                message = getMessage( e.getLinkedException() );
485            }
486
487            throw new ModelException( message, e );
488        }
489    }
490
491    private static String getMessage( final String key, final Object... args )
492    {
493        return MessageFormat.format( ResourceBundle.getBundle(
494            DefaultModelProcessor.class.getName().replace( '.', '/' ), Locale.getDefault() ).getString( key ), args );
495
496    }
497
498    private static String getMessage( final Throwable t )
499    {
500        return t != null
501               ? t.getMessage() != null && t.getMessage().trim().length() > 0
502                 ? t.getMessage()
503                 : getMessage( t.getCause() )
504               : null;
505
506    }
507
508}