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