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