001    /*
002     *   Copyright (c) 2009 The JOMC Project
003     *   Copyright (c) 2005 Christian Schulte <cs@jomc.org>
004     *   All rights reserved.
005     *
006     *   Redistribution and use in source and binary forms, with or without
007     *   modification, are permitted provided that the following conditions
008     *   are met:
009     *
010     *     o Redistributions of source code must retain the above copyright
011     *       notice, this list of conditions and the following disclaimer.
012     *
013     *     o Redistributions in binary form must reproduce the above copyright
014     *       notice, this list of conditions and the following disclaimer in
015     *       the documentation and/or other materials provided with the
016     *       distribution.
017     *
018     *   THIS SOFTWARE IS PROVIDED BY THE JOMC PROJECT AND CONTRIBUTORS "AS IS"
019     *   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020     *   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021     *   PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE JOMC PROJECT OR
022     *   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023     *   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024     *   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025     *   OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026     *   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
027     *   OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
028     *   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029     *
030     *   $Id: DefaultModelManager.java 891 2009-11-02 03:40:00Z schulte2005 $
031     *
032     */
033    package org.jomc.model;
034    
035    import java.io.IOException;
036    import java.io.InputStream;
037    import java.io.Reader;
038    import java.net.URI;
039    import java.net.URISyntaxException;
040    import java.net.URL;
041    import java.text.MessageFormat;
042    import java.util.ArrayList;
043    import java.util.Enumeration;
044    import java.util.HashSet;
045    import java.util.Iterator;
046    import java.util.LinkedList;
047    import java.util.List;
048    import java.util.Locale;
049    import java.util.Map;
050    import java.util.ResourceBundle;
051    import java.util.Set;
052    import java.util.jar.Attributes;
053    import java.util.jar.Manifest;
054    import java.util.logging.Level;
055    import javax.xml.XMLConstants;
056    import javax.xml.bind.JAXBContext;
057    import javax.xml.bind.JAXBElement;
058    import javax.xml.bind.JAXBException;
059    import javax.xml.bind.Marshaller;
060    import javax.xml.bind.Unmarshaller;
061    import javax.xml.transform.ErrorListener;
062    import javax.xml.transform.Source;
063    import javax.xml.transform.Transformer;
064    import javax.xml.transform.TransformerConfigurationException;
065    import javax.xml.transform.TransformerException;
066    import javax.xml.transform.TransformerFactory;
067    import javax.xml.transform.URIResolver;
068    import javax.xml.transform.sax.SAXSource;
069    import javax.xml.transform.stream.StreamSource;
070    import javax.xml.validation.SchemaFactory;
071    import org.jomc.model.bootstrap.Schema;
072    import org.jomc.model.bootstrap.Schemas;
073    import org.w3c.dom.ls.LSInput;
074    import org.w3c.dom.ls.LSResourceResolver;
075    import org.xml.sax.EntityResolver;
076    import org.xml.sax.InputSource;
077    import org.xml.sax.SAXException;
078    
079    /**
080     * Default {@code ModelManager} implementation.
081     *
082     * <p><b>Schema management</b><ul>
083     * <li>{@link #getBootstrapSchema() }</li>
084     * <li>{@link #getBootstrapContext() }</li>
085     * </ul></p>
086     *
087     * <p><b>Resource management</b><ul>
088     * <li>{@link #getClasspathModules(java.lang.ClassLoader, java.lang.String) }</li>
089     * <li>{@link #getClasspathSchemas(java.lang.ClassLoader, java.lang.String) }</li>
090     * <li>{@link #getClasspathTransformers(java.lang.ClassLoader, java.lang.String) }</li>
091     * </ul></p>
092     *
093     * <p><b>Log management</b><ul>
094     * <li>{@link #getLogLevel() }</li>
095     * <li>{@link #getListeners() }</li>
096     * <li>{@link #log(java.util.logging.Level, java.lang.String, java.lang.Throwable) }</li>
097     * </ul></p>
098     *
099     * @author <a href="mailto:cs@jomc.org">Christian Schulte</a>
100     * @version $Id: DefaultModelManager.java 891 2009-11-02 03:40:00Z schulte2005 $
101     */
102    public class DefaultModelManager implements ModelManager
103    {
104        // SECTION-START[ModelManager]
105    
106        public EntityResolver getEntityResolver( final ClassLoader classLoader )
107        {
108            if ( classLoader == null )
109            {
110                throw new NullPointerException( "classLoader" );
111            }
112    
113            return new EntityResolver()
114            {
115    
116                public InputSource resolveEntity( final String publicId, final String systemId )
117                    throws SAXException, IOException
118                {
119                    if ( systemId == null )
120                    {
121                        throw new NullPointerException( "systemId" );
122                    }
123    
124                    InputSource schemaSource = null;
125    
126                    try
127                    {
128                        Schema s = null;
129                        final Schemas classpathSchemas = getClasspathSchemas( classLoader, getDefaultSchemaLocation() );
130    
131                        if ( publicId != null )
132                        {
133                            s = classpathSchemas.getSchemaByPublicId( publicId );
134                        }
135                        if ( s == null )
136                        {
137                            s = classpathSchemas.getSchemaBySystemId( systemId );
138                        }
139    
140                        if ( s != null )
141                        {
142                            schemaSource = new InputSource();
143                            schemaSource.setPublicId( s.getPublicId() != null ? s.getPublicId() : publicId );
144                            schemaSource.setSystemId( s.getSystemId() );
145    
146                            if ( s.getClasspathId() != null )
147                            {
148                                final URL resource = classLoader.getResource( s.getClasspathId() );
149    
150                                if ( resource != null )
151                                {
152                                    schemaSource.setSystemId( resource.toExternalForm() );
153                                }
154                                else
155                                {
156                                    if ( isLoggable( Level.WARNING ) )
157                                    {
158                                        log( Level.WARNING, getMessage( "resourceNotFound", new Object[]
159                                            {
160                                                s.getClasspathId()
161                                            } ), null );
162    
163                                    }
164                                }
165                            }
166    
167                            if ( isLoggable( Level.FINE ) )
168                            {
169                                log( Level.FINE, getMessage( "resolutionInfo", new Object[]
170                                    {
171                                        publicId + ":" + systemId,
172                                        schemaSource.getPublicId() + ":" + schemaSource.getSystemId()
173                                    } ), null );
174    
175                            }
176                        }
177    
178                        if ( schemaSource == null )
179                        {
180                            final URI systemUri = new URI( systemId );
181                            String schemaName = systemUri.getPath();
182                            if ( schemaName != null )
183                            {
184                                final int lastIndexOfSlash = schemaName.lastIndexOf( '/' );
185                                if ( lastIndexOfSlash != -1 && lastIndexOfSlash < schemaName.length() )
186                                {
187                                    schemaName = schemaName.substring( lastIndexOfSlash + 1 );
188                                }
189    
190                                for ( URL url : getSchemaResources( classLoader ) )
191                                {
192                                    if ( url.getPath().endsWith( schemaName ) )
193                                    {
194                                        schemaSource = new InputSource();
195                                        schemaSource.setPublicId( publicId );
196                                        schemaSource.setSystemId( url.toExternalForm() );
197    
198                                        if ( isLoggable( Level.FINE ) )
199                                        {
200                                            log( Level.FINE, getMessage( "resolutionInfo", new Object[]
201                                                {
202                                                    systemUri.toASCIIString(),
203                                                    schemaSource.getSystemId()
204                                                } ), null );
205    
206                                        }
207    
208                                        break;
209                                    }
210                                }
211                            }
212                            else
213                            {
214                                if ( isLoggable( Level.WARNING ) )
215                                {
216                                    log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[]
217                                        {
218                                            systemId, systemUri.toASCIIString()
219                                        } ), null );
220    
221                                }
222    
223                                schemaSource = null;
224                            }
225                        }
226                    }
227                    catch ( final URISyntaxException e )
228                    {
229                        if ( isLoggable( Level.WARNING ) )
230                        {
231                            log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[]
232                                {
233                                    systemId, e.getMessage()
234                                } ), null );
235    
236                        }
237    
238                        schemaSource = null;
239                    }
240                    catch ( final JAXBException e )
241                    {
242                        throw (IOException) new IOException( e.getMessage() ).initCause( e );
243                    }
244    
245                    return schemaSource;
246                }
247    
248            };
249        }
250    
251        public LSResourceResolver getResourceResolver( final ClassLoader classLoader )
252        {
253            if ( classLoader == null )
254            {
255                throw new NullPointerException( "classLoader" );
256            }
257    
258            return new LSResourceResolver()
259            {
260    
261                public LSInput resolveResource( final String type, final String namespaceURI, final String publicId,
262                                                final String systemId, final String baseURI )
263                {
264                    if ( XMLConstants.W3C_XML_SCHEMA_NS_URI.equals( type ) )
265                    {
266                        try
267                        {
268                            final InputSource schemaSource = getEntityResolver( classLoader ).resolveEntity(
269                                namespaceURI == null ? publicId : namespaceURI, systemId == null ? "" : systemId );
270    
271                            if ( schemaSource != null )
272                            {
273                                return new LSInput()
274                                {
275    
276                                    public Reader getCharacterStream()
277                                    {
278                                        return schemaSource.getCharacterStream();
279                                    }
280    
281                                    public void setCharacterStream( final Reader characterStream )
282                                    {
283                                        throw new UnsupportedOperationException();
284                                    }
285    
286                                    public InputStream getByteStream()
287                                    {
288                                        return schemaSource.getByteStream();
289                                    }
290    
291                                    public void setByteStream( final InputStream byteStream )
292                                    {
293                                        throw new UnsupportedOperationException();
294                                    }
295    
296                                    public String getStringData()
297                                    {
298                                        return null;
299                                    }
300    
301                                    public void setStringData( final String stringData )
302                                    {
303                                        throw new UnsupportedOperationException();
304                                    }
305    
306                                    public String getSystemId()
307                                    {
308                                        return schemaSource.getSystemId();
309                                    }
310    
311                                    public void setSystemId( final String systemId )
312                                    {
313                                        throw new UnsupportedOperationException();
314                                    }
315    
316                                    public String getPublicId()
317                                    {
318                                        return schemaSource.getPublicId();
319                                    }
320    
321                                    public void setPublicId( final String publicId )
322                                    {
323                                        throw new UnsupportedOperationException();
324                                    }
325    
326                                    public String getBaseURI()
327                                    {
328                                        return baseURI;
329                                    }
330    
331                                    public void setBaseURI( final String baseURI )
332                                    {
333                                        throw new UnsupportedOperationException();
334                                    }
335    
336                                    public String getEncoding()
337                                    {
338                                        return schemaSource.getEncoding();
339                                    }
340    
341                                    public void setEncoding( final String encoding )
342                                    {
343                                        throw new UnsupportedOperationException();
344                                    }
345    
346                                    public boolean getCertifiedText()
347                                    {
348                                        return false;
349                                    }
350    
351                                    public void setCertifiedText( final boolean certifiedText )
352                                    {
353                                        throw new UnsupportedOperationException();
354                                    }
355    
356                                };
357                            }
358    
359                        }
360                        catch ( final SAXException e )
361                        {
362                            if ( isLoggable( Level.WARNING ) )
363                            {
364                                log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[]
365                                    {
366                                        systemId, e.getMessage()
367                                    } ), null );
368    
369                            }
370                        }
371                        catch ( final IOException e )
372                        {
373                            if ( isLoggable( Level.WARNING ) )
374                            {
375                                log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[]
376                                    {
377                                        systemId, e.getMessage()
378                                    } ), null );
379    
380                            }
381                        }
382                    }
383                    else if ( isLoggable( Level.WARNING ) )
384                    {
385                        log( Level.WARNING, getMessage( "unsupportedResourceType", new Object[]
386                            {
387                                type
388                            } ), null );
389    
390                    }
391    
392                    return null;
393                }
394    
395            };
396        }
397    
398        public javax.xml.validation.Schema getSchema( final ClassLoader classLoader )
399            throws IOException, SAXException, JAXBException
400        {
401            if ( classLoader == null )
402            {
403                throw new NullPointerException( "classLoader" );
404            }
405    
406            final SchemaFactory f = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI );
407            final Schemas schemas = this.getClasspathSchemas( classLoader, getDefaultSchemaLocation() );
408            final List<Source> sources = new ArrayList<Source>( schemas.getSchema().size() );
409            final EntityResolver entityResolver = this.getEntityResolver( classLoader );
410    
411            for ( Schema s : schemas.getSchema() )
412            {
413                final InputSource inputSource = entityResolver.resolveEntity( s.getPublicId(), s.getSystemId() );
414    
415                if ( inputSource != null )
416                {
417                    sources.add( new SAXSource( inputSource ) );
418                }
419            }
420    
421            return f.newSchema( sources.toArray( new Source[ sources.size() ] ) );
422        }
423    
424        public JAXBContext getContext( final ClassLoader classLoader ) throws IOException, SAXException, JAXBException
425        {
426            if ( classLoader == null )
427            {
428                throw new NullPointerException( "classLoader" );
429            }
430    
431            final StringBuilder packageNames = new StringBuilder();
432    
433            for ( final Iterator<Schema> s = this.getClasspathSchemas( classLoader, getDefaultSchemaLocation() ).
434                getSchema().iterator(); s.hasNext(); )
435            {
436                final Schema schema = s.next();
437                if ( schema.getContextId() != null )
438                {
439                    packageNames.append( ':' ).append( schema.getContextId() );
440                    if ( this.isLoggable( Level.FINE ) )
441                    {
442                        this.log( Level.FINE, this.getMessage( "addingContext", new Object[]
443                            {
444                                schema.getContextId()
445                            } ), null );
446    
447                    }
448                }
449            }
450    
451            if ( packageNames.length() == 0 )
452            {
453                throw new IOException( this.getMessage( "missingSchemas", new Object[]
454                    {
455                        getDefaultSchemaLocation()
456                    } ) );
457    
458            }
459    
460            return JAXBContext.newInstance( packageNames.toString().substring( 1 ), classLoader );
461        }
462    
463        public Marshaller getMarshaller( final ClassLoader classLoader ) throws IOException, SAXException, JAXBException
464        {
465            if ( classLoader == null )
466            {
467                throw new NullPointerException( "classLoader" );
468            }
469    
470            final StringBuilder packageNames = new StringBuilder();
471            final StringBuilder schemaLocation = new StringBuilder();
472    
473            for ( final Iterator<Schema> s = this.getClasspathSchemas( classLoader, getDefaultSchemaLocation() ).
474                getSchema().iterator(); s.hasNext(); )
475            {
476                final Schema schema = s.next();
477                if ( schema.getContextId() != null )
478                {
479                    packageNames.append( ':' ).append( schema.getContextId() );
480                }
481                if ( schema.getPublicId() != null && schema.getSystemId() != null )
482                {
483                    schemaLocation.append( ' ' ).append( schema.getPublicId() ).append( ' ' ).
484                        append( schema.getSystemId() );
485    
486                }
487            }
488    
489            if ( packageNames.length() == 0 )
490            {
491                throw new IOException( this.getMessage( "missingSchemas", new Object[]
492                    {
493                        getDefaultSchemaLocation()
494                    } ) );
495    
496            }
497    
498            final JAXBContext context = JAXBContext.newInstance( packageNames.toString().substring( 1 ), classLoader );
499            final Marshaller m = context.createMarshaller();
500    
501            if ( schemaLocation.length() != 0 )
502            {
503                m.setProperty( Marshaller.JAXB_SCHEMA_LOCATION, schemaLocation.toString().substring( 1 ) );
504            }
505    
506            return m;
507        }
508    
509        public Unmarshaller getUnmarshaller( final ClassLoader classLoader ) throws IOException, SAXException, JAXBException
510        {
511            if ( classLoader == null )
512            {
513                throw new NullPointerException( "classLoader" );
514            }
515    
516            return this.getContext( classLoader ).createUnmarshaller();
517        }
518    
519        // SECTION-END
520        // SECTION-START[DefaultModelManager]
521        /** Listener interface. */
522        public interface Listener
523        {
524    
525            /**
526             * Get called on logging.
527             *
528             * @param level The level of the event.
529             * @param message The message of the event or {@code null}.
530             * @param t The throwable of the event or {@code null}.
531             *
532             * @throws NullPointerException if {@code level} is {@code null}.
533             */
534            void onLog( Level level, String message, Throwable t );
535    
536        }
537    
538        /**
539         * Log level events are logged at by default.
540         * @see #getDefaultLogLevel()
541         */
542        private static final Level DEFAULT_LOG_LEVEL = Level.WARNING;
543    
544        /**
545         * Classpath location searched for modules by default.
546         * @see #getDefaultModuleLocation()
547         */
548        private static final String DEFAULT_MODULE_LOCATION = "META-INF/jomc.xml";
549    
550        /**
551         * Classpath location searched for transformers by default.
552         * @see #getDefaultTransformerLocation()
553         */
554        private static final String DEFAULT_TRANSFORMER_LOCATION = "META-INF/jomc.xslt";
555    
556        /** Classpath location of the bootstrap schema. */
557        private static final String BOOTSTRAP_SCHEMA_LOCATION =
558            Schemas.class.getPackage().getName().replace( '.', '/' ) + "/jomc-bootstrap-1.0.xsd";
559    
560        /** Value for property {@link Marshaller#JAXB_SCHEMA_LOCATION} of any bootstrap JAXB marshaller instance. */
561        private static final String BOOTSTRAP_JAXB_SCHEMA_LOCATION =
562            "http://jomc.org/model/bootstrap http://jomc.org/model/bootstrap/jomc-bootstrap-1.0.xsd";
563    
564        /**
565         * Classpath location searched for schemas by default.
566         * @see #getDefaultSchemaLocation()
567         */
568        private static final String DEFAULT_SCHEMA_LOCATION = "META-INF/jomc-bootstrap.xml";
569    
570        /** JAXB context of the bootstrap schema. */
571        private static final String BOOTSTRAP_CONTEXT = Schemas.class.getPackage().getName();
572    
573        /** Supported schema name extensions. */
574        private static final String[] SCHEMA_EXTENSIONS = new String[]
575        {
576            "xsd"
577        };
578    
579        /** Default log level. */
580        private static volatile Level defaultLogLevel;
581    
582        /** Default module location. */
583        private static volatile String defaultModuleLocation;
584    
585        /** Default schema location. */
586        private static volatile String defaultSchemaLocation;
587    
588        /** Default transformer location. */
589        private static volatile String defaultTransformerLocation;
590    
591        /** The listeners of the instance. */
592        private List<Listener> listeners;
593    
594        /** Log level of the instance. */
595        private Level logLevel;
596    
597        /** Creates a new {@code DefaultModelManager} instance. */
598        public DefaultModelManager()
599        {
600            super();
601        }
602    
603        /**
604         * Gets the list of registered listeners.
605         * <p>This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
606         * to the returned list will be present inside the object. This is why there is no {@code set} method for the
607         * listeners property.</p>
608         *
609         * @return The list of registered listeners.
610         *
611         * @see #log(java.util.logging.Level, java.lang.String, java.lang.Throwable)
612         */
613        public List<Listener> getListeners()
614        {
615            if ( this.listeners == null )
616            {
617                this.listeners = new LinkedList<Listener>();
618            }
619    
620            return this.listeners;
621        }
622    
623        /**
624         * Gets the default log level events are logged at.
625         * <p>The default log level is controlled by system property
626         * {@code org.jomc.model.DefaultModelManager.defaultLogLevel} holding the log level to log events at by default.
627         * If that property is not set, the {@code WARNING} default is returned.</p>
628         *
629         * @return The log level events are logged at by default.
630         *
631         * @see #getLogLevel()
632         * @see Level#parse(java.lang.String)
633         */
634        public static Level getDefaultLogLevel()
635        {
636            if ( defaultLogLevel == null )
637            {
638                defaultLogLevel = Level.parse( System.getProperty( "org.jomc.model.DefaultModelManager.defaultLogLevel",
639                                                                   DEFAULT_LOG_LEVEL.getName() ) );
640    
641            }
642    
643            return defaultLogLevel;
644        }
645    
646        /**
647         * Sets the default log level events are logged at.
648         *
649         * @param value The new default level events are logged at or {@code null}.
650         *
651         * @see #getDefaultLogLevel()
652         */
653        public static void setDefaultLogLevel( final Level value )
654        {
655            defaultLogLevel = value;
656        }
657    
658        /**
659         * Gets the log level of the instance.
660         *
661         * @return The log level of the instance.
662         *
663         * @see #getDefaultLogLevel()
664         * @see #setLogLevel(java.util.logging.Level)
665         * @see #isLoggable(java.util.logging.Level)
666         */
667        public Level getLogLevel()
668        {
669            if ( this.logLevel == null )
670            {
671                this.logLevel = getDefaultLogLevel();
672                this.log( Level.CONFIG, this.getMessage( "defaultLogLevelInfo", new Object[]
673                    {
674                        this.getClass().getCanonicalName(), this.logLevel.getLocalizedName()
675                    } ), null );
676    
677            }
678    
679            return this.logLevel;
680        }
681    
682        /**
683         * Sets the log level of the instance.
684         *
685         * @param value The new log level of the instance or {@code null}.
686         *
687         * @see #getLogLevel()
688         * @see #isLoggable(java.util.logging.Level)
689         */
690        public void setLogLevel( final Level value )
691        {
692            this.logLevel = value;
693        }
694    
695        /**
696         * Checks if a message at a given level is provided to the listeners of the instance.
697         *
698         * @param level The level to test.
699         *
700         * @return {@code true} if messages at {@code level} are provided to the listeners of the instance;
701         * {@code false} if messages at {@code level} are not provided to the listeners of the instance.
702         *
703         * @throws NullPointerException if {@code level} is {@code null}.
704         *
705         * @see #getLogLevel()
706         * @see #setLogLevel(java.util.logging.Level)
707         */
708        public boolean isLoggable( final Level level )
709        {
710            if ( level == null )
711            {
712                throw new NullPointerException( "level" );
713            }
714    
715            return level.intValue() >= this.getLogLevel().intValue();
716        }
717    
718        /**
719         * Notifies registered listeners.
720         *
721         * @param level The level of the event.
722         * @param message The message of the event or {@code null}.
723         * @param throwable The throwable of the event {@code null}.
724         *
725         * @throws NullPointerException if {@code level} is {@code null}.
726         *
727         * @see #getListeners()
728         * @see #isLoggable(java.util.logging.Level)
729         */
730        protected void log( final Level level, final String message, final Throwable throwable )
731        {
732            if ( level == null )
733            {
734                throw new NullPointerException( "level" );
735            }
736    
737            if ( this.isLoggable( level ) )
738            {
739                for ( Listener l : this.getListeners() )
740                {
741                    l.onLog( level, message, throwable );
742                }
743            }
744        }
745    
746        /**
747         * Gets a new bootstrap JAXB context instance.
748         *
749         * @return A new bootstrap JAXB context instance.
750         *
751         * @throws JAXBException if creating a new bootstrap JAXB context instance fails.
752         */
753        public JAXBContext getBootstrapContext() throws JAXBException
754        {
755            return JAXBContext.newInstance( BOOTSTRAP_CONTEXT );
756        }
757    
758        /**
759         * Gets a new bootstrap JAXB marshaller instance.
760         *
761         * @return A new bootstrap JAXB marshaller instance.
762         *
763         * @throws JAXBException if creating a new bootstrap JAXB marshaller instance fails.
764         */
765        public Marshaller getBootstrapMarshaller() throws JAXBException
766        {
767            final Marshaller m = this.getBootstrapContext().createMarshaller();
768            m.setProperty( Marshaller.JAXB_SCHEMA_LOCATION, BOOTSTRAP_JAXB_SCHEMA_LOCATION );
769            return m;
770        }
771    
772        /**
773         * Gets a new bootstrap JAXB unmarshaller instance.
774         *
775         * @return A new bootstrap JAXB unmarshaller instance.
776         *
777         * @throws JAXBException if creating a new bootstrap JAXB unmarshaller instance fails.
778         */
779        public Unmarshaller getBootstrapUnmarshaller() throws JAXBException
780        {
781            return this.getBootstrapContext().createUnmarshaller();
782        }
783    
784        /**
785         * Gets a new bootstrap JAXP schema instance.
786         *
787         * @return A new bootstrap JAXP schema instance.
788         *
789         * @throws SAXException if parsing the bootstrap schema fails.
790         */
791        public javax.xml.validation.Schema getBootstrapSchema() throws SAXException
792        {
793            final ClassLoader classLoader = this.getClass().getClassLoader();
794            return SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ).newSchema(
795                classLoader != null
796                ? classLoader.getResource( BOOTSTRAP_SCHEMA_LOCATION )
797                : ClassLoader.getSystemResource( BOOTSTRAP_SCHEMA_LOCATION ) );
798    
799        }
800    
801        /**
802         * Gets the default location searched for schema resources.
803         * <p>The default schema location is controlled by system property
804         * {@code org.jomc.model.DefaultModelManager.defaultSchemaLocation} holding the location to search for schema
805         * resources by default. If that property is not set, the {@code META-INF/jomc-bootstrap.xml} default is returned.
806         * </p>
807         *
808         * @return The location searched for schema resources by default.
809         *
810         * @see #getClasspathSchemas(java.lang.ClassLoader, java.lang.String)
811         */
812        public static String getDefaultSchemaLocation()
813        {
814            if ( defaultSchemaLocation == null )
815            {
816                defaultSchemaLocation = System.getProperty( "org.jomc.model.DefaultModelManager.defaultSchemaLocation",
817                                                            DEFAULT_SCHEMA_LOCATION );
818    
819            }
820    
821            return defaultSchemaLocation;
822        }
823    
824        /**
825         * Sets the default location searched for schema resources.
826         *
827         * @param value The new default location to search for schema resources or {@code null}.
828         *
829         * @see #getDefaultSchemaLocation()
830         */
831        public static void setDefaultSchemaLocation( final String value )
832        {
833            defaultSchemaLocation = value;
834        }
835    
836        /**
837         * Gets schemas by searching a given class loader for resources.
838         *
839         * @param classLoader The class loader to search for resources.
840         * @param location The location to search at.
841         *
842         * @return All schemas found at {@code location} by querying {@code classLoader}.
843         *
844         * @throws NullPointerException if {@code classLoader} or {@code location} is {@code null}.
845         * @throws IOException if reading resources fails.
846         * @throws SAXException if parsing schema resources fails.
847         * @throws JAXBException if unmarshalling schema resources fails.
848         *
849         * @see #getDefaultSchemaLocation()
850         */
851        public Schemas getClasspathSchemas( final ClassLoader classLoader, final String location )
852            throws IOException, JAXBException, SAXException
853        {
854            if ( classLoader == null )
855            {
856                throw new NullPointerException( "classLoader" );
857            }
858            if ( location == null )
859            {
860                throw new NullPointerException( "location" );
861            }
862    
863            final long t0 = System.currentTimeMillis();
864            final Schemas schemas = new Schemas();
865            final JAXBContext ctx = JAXBContext.newInstance( BOOTSTRAP_CONTEXT );
866            final Unmarshaller u = ctx.createUnmarshaller();
867            final Enumeration<URL> e = classLoader.getResources( location );
868            u.setSchema( this.getBootstrapSchema() );
869            int count = 0;
870    
871            while ( e.hasMoreElements() )
872            {
873                count++;
874                final URL url = e.nextElement();
875    
876                if ( this.isLoggable( Level.FINE ) )
877                {
878                    this.log( Level.FINE, this.getMessage( "processing", new Object[]
879                        {
880                            url.toExternalForm()
881                        } ), null );
882    
883                }
884    
885                Object content = u.unmarshal( url );
886                if ( content instanceof JAXBElement )
887                {
888                    content = ( (JAXBElement) content ).getValue();
889                }
890    
891                if ( content instanceof Schema )
892                {
893                    final Schema s = (Schema) content;
894                    if ( this.isLoggable( Level.FINE ) )
895                    {
896                        this.log( Level.FINE, this.getMessage( "addingSchema", new Object[]
897                            {
898                                s.getPublicId(), s.getSystemId(), s.getContextId(), s.getClasspathId()
899                            } ), null );
900    
901                    }
902    
903                    schemas.getSchema().add( s );
904                }
905                else if ( content instanceof Schemas )
906                {
907                    for ( Schema s : ( (Schemas) content ).getSchema() )
908                    {
909                        if ( this.isLoggable( Level.FINE ) )
910                        {
911                            this.log( Level.FINE, this.getMessage( "addingSchema", new Object[]
912                                {
913                                    s.getPublicId(), s.getSystemId(), s.getContextId(), s.getClasspathId()
914                                } ), null );
915    
916                        }
917    
918                        schemas.getSchema().add( s );
919                    }
920                }
921            }
922    
923            if ( this.isLoggable( Level.FINE ) )
924            {
925                this.log( Level.FINE, this.getMessage( "classpathReport", new Object[]
926                    {
927                        count, location, Long.valueOf( System.currentTimeMillis() - t0 )
928                    } ), null );
929    
930            }
931    
932            return schemas;
933        }
934    
935        /**
936         * Gets the default location searched for module resources.
937         * <p>The default module location is controlled by system property
938         * {@code org.jomc.model.DefaultModelManager.defaultModuleLocation} holding the location to search for module
939         * resources by default. If that property is not set, the {@code META-INF/jomc.xml} default is returned.</p>
940         *
941         * @return The location searched for module resources by default.
942         *
943         * @see #getClasspathModules(java.lang.ClassLoader, java.lang.String)
944         */
945        public static String getDefaultModuleLocation()
946        {
947            if ( defaultModuleLocation == null )
948            {
949                defaultModuleLocation = System.getProperty( "org.jomc.model.DefaultModelManager.defaultModuleLocation",
950                                                            DEFAULT_MODULE_LOCATION );
951    
952            }
953    
954            return defaultModuleLocation;
955        }
956    
957        /**
958         * Sets the default location searched for module resources.
959         *
960         * @param value The new default location to search for module resources or {@code null}.
961         *
962         * @see #getDefaultModuleLocation()
963         */
964        public static void setDefaultModuleLocation( final String value )
965        {
966            defaultModuleLocation = value;
967        }
968    
969        /**
970         * Gets modules by searching a given class loader for resources.
971         * <p><b>Note:</b><br/>
972         * This method does not validate the modules.</p>
973         *
974         * @param classLoader The class loader to search for resources.
975         * @param location The location to search at.
976         *
977         * @return All modules found at {@code location} by querying {@code classLoader}.
978         *
979         * @throws NullPointerException if {@code classLoader} or {@code location} is {@code null}.
980         * @throws IOException if reading resources fails.
981         * @throws SAXException if parsing schema resources fails.
982         * @throws JAXBException if unmarshalling schema resources fails.
983         *
984         * @see #getDefaultModuleLocation()
985         * @see ModelObjectValidator
986         */
987        public Modules getClasspathModules( final ClassLoader classLoader, final String location )
988            throws IOException, SAXException, JAXBException
989        {
990            if ( classLoader == null )
991            {
992                throw new NullPointerException( "classLoader" );
993            }
994            if ( location == null )
995            {
996                throw new NullPointerException( "location" );
997            }
998    
999            final long t0 = System.currentTimeMillis();
1000            final Text text = new Text();
1001            text.setLanguage( "en" );
1002            text.setValue( this.getMessage( "classpathModulesInfo", new Object[]
1003                {
1004                    location
1005                } ) );
1006    
1007            final Modules modules = new Modules();
1008            modules.setDocumentation( new Texts() );
1009            modules.getDocumentation().setDefaultLanguage( "en" );
1010            modules.getDocumentation().getText().add( text );
1011    
1012            final Unmarshaller u = this.getUnmarshaller( classLoader );
1013            final Enumeration<URL> resources = classLoader.getResources( location );
1014    
1015            int count = 0;
1016            while ( resources.hasMoreElements() )
1017            {
1018                count++;
1019                final URL url = resources.nextElement();
1020    
1021                if ( this.isLoggable( Level.FINE ) )
1022                {
1023                    this.log( Level.FINE, this.getMessage( "processing", new Object[]
1024                        {
1025                            url.toExternalForm()
1026                        } ), null );
1027    
1028                }
1029    
1030                Object content = u.unmarshal( url );
1031                if ( content instanceof JAXBElement )
1032                {
1033                    content = ( (JAXBElement) content ).getValue();
1034                }
1035    
1036                if ( content instanceof Module )
1037                {
1038                    final Module m = (Module) content;
1039                    if ( this.isLoggable( Level.FINE ) )
1040                    {
1041                        this.log( Level.FINE, this.getMessage( "addingModule", new Object[]
1042                            {
1043                                m.getName(), m.getVersion() == null ? "" : m.getVersion()
1044                            } ), null );
1045    
1046                    }
1047    
1048                    modules.getModule().add( m );
1049                }
1050                else if ( this.isLoggable( Level.WARNING ) )
1051                {
1052                    this.log( Level.WARNING, this.getMessage( "ignoringDocument", new Object[]
1053                        {
1054                            content == null ? "<>" : content.toString(), url.toExternalForm()
1055                        } ), null );
1056    
1057                }
1058            }
1059    
1060            if ( this.isLoggable( Level.FINE ) )
1061            {
1062                this.log( Level.FINE, this.getMessage( "classpathReport", new Object[]
1063                    {
1064                        count, location, Long.valueOf( System.currentTimeMillis() - t0 )
1065                    } ), null );
1066    
1067            }
1068    
1069            return modules;
1070        }
1071    
1072        /**
1073         * Gets the default location searched for transformer resources.
1074         * <p>The default transformer location is controlled by system property
1075         * {@code org.jomc.model.DefaultModelManager.defaultTransformerLocation} holding the location to search for
1076         * transformer resources by default. If that property is not set, the {@code META-INF/jomc.xslt} default is
1077         * returned.</p>
1078         *
1079         * @return The location searched for transformer resources by default.
1080         *
1081         * @see #getClasspathTransformers(java.lang.ClassLoader, java.lang.String)
1082         */
1083        public static String getDefaultTransformerLocation()
1084        {
1085            if ( defaultTransformerLocation == null )
1086            {
1087                defaultTransformerLocation =
1088                    System.getProperty( "org.jomc.model.DefaultModelManager.defaultTransformerLocation",
1089                                        DEFAULT_TRANSFORMER_LOCATION );
1090    
1091            }
1092    
1093            return defaultTransformerLocation;
1094        }
1095    
1096        /**
1097         * Sets the default location searched for transformer resources.
1098         *
1099         * @param value The new default location to search for transformer resources or {@code null}.
1100         *
1101         * @see #getDefaultTransformerLocation()
1102         */
1103        public static void setDefaultTransformerLocation( final String value )
1104        {
1105            defaultTransformerLocation = value;
1106        }
1107    
1108        /**
1109         * Gets transformers by searching a given class loader for resources.
1110         *
1111         * @param classLoader The class loader to search for resources.
1112         * @param location The location to search at.
1113         *
1114         * @return All transformers found at {@code location} by querying {@code classLoader}.
1115         *
1116         * @throws NullPointerException if {@code classLoader} or {@code location} is {@code null}.
1117         * @throws IOException if reading resources fails.
1118         * @throws TransformerConfigurationException if getting the transformers fails.
1119         *
1120         * @see #getDefaultTransformerLocation()
1121         */
1122        public List<Transformer> getClasspathTransformers( final ClassLoader classLoader, final String location )
1123            throws IOException, TransformerConfigurationException
1124        {
1125            if ( classLoader == null )
1126            {
1127                throw new NullPointerException( "classLoader" );
1128            }
1129            if ( location == null )
1130            {
1131                throw new NullPointerException( "location" );
1132            }
1133    
1134            final long t0 = System.currentTimeMillis();
1135            final List<Transformer> transformers = new LinkedList<Transformer>();
1136            final TransformerFactory transformerFactory = TransformerFactory.newInstance();
1137            final Enumeration<URL> resources = classLoader.getResources( location );
1138            final ErrorListener errorListener = new ErrorListener()
1139            {
1140    
1141                public void warning( final TransformerException exception ) throws TransformerException
1142                {
1143                    if ( isLoggable( Level.WARNING ) )
1144                    {
1145                        log( Level.WARNING, exception.getMessage(), exception );
1146                    }
1147                }
1148    
1149                public void error( final TransformerException exception ) throws TransformerException
1150                {
1151                    if ( isLoggable( Level.SEVERE ) )
1152                    {
1153                        log( Level.SEVERE, exception.getMessage(), exception );
1154                    }
1155    
1156                    throw exception;
1157                }
1158    
1159                public void fatalError( final TransformerException exception ) throws TransformerException
1160                {
1161                    if ( isLoggable( Level.SEVERE ) )
1162                    {
1163                        log( Level.SEVERE, exception.getMessage(), exception );
1164                    }
1165    
1166                    throw exception;
1167                }
1168    
1169            };
1170    
1171            final URIResolver uriResolver = new URIResolver()
1172            {
1173    
1174                public Source resolve( final String href, final String base ) throws TransformerException
1175                {
1176                    try
1177                    {
1178                        final InputSource inputSource = getEntityResolver( classLoader ).resolveEntity( null, href );
1179    
1180                        if ( inputSource != null )
1181                        {
1182                            return new SAXSource( inputSource );
1183                        }
1184    
1185                        return null;
1186                    }
1187                    catch ( final SAXException e )
1188                    {
1189                        if ( isLoggable( Level.SEVERE ) )
1190                        {
1191                            log( Level.SEVERE, e.getMessage(), e );
1192                        }
1193    
1194                        throw new TransformerException( e );
1195                    }
1196                    catch ( final IOException e )
1197                    {
1198                        if ( isLoggable( Level.SEVERE ) )
1199                        {
1200                            log( Level.SEVERE, e.getMessage(), e );
1201                        }
1202    
1203                        throw new TransformerException( e );
1204                    }
1205                }
1206    
1207            };
1208    
1209            transformerFactory.setErrorListener( errorListener );
1210            transformerFactory.setURIResolver( uriResolver );
1211    
1212            int count = 0;
1213            while ( resources.hasMoreElements() )
1214            {
1215                count++;
1216                final URL url = resources.nextElement();
1217    
1218                if ( this.isLoggable( Level.FINE ) )
1219                {
1220                    this.log( Level.FINE, this.getMessage( "processing", new Object[]
1221                        {
1222                            url.toExternalForm()
1223                        } ), null );
1224    
1225                }
1226    
1227                final InputStream in = url.openStream();
1228                final Transformer transformer = transformerFactory.newTransformer( new StreamSource( in ) );
1229                in.close();
1230    
1231                transformer.setErrorListener( errorListener );
1232                transformer.setURIResolver( uriResolver );
1233                transformers.add( transformer );
1234            }
1235    
1236            if ( this.isLoggable( Level.FINE ) )
1237            {
1238                this.log( Level.FINE, this.getMessage( "classpathReport", new Object[]
1239                    {
1240                        count, location, Long.valueOf( System.currentTimeMillis() - t0 )
1241                    } ), null );
1242    
1243            }
1244    
1245            return transformers;
1246        }
1247    
1248        /**
1249         * Searches all available {@code META-INF/MANIFEST.MF} resources from a given class loader and returns a set
1250         * containing URLs of entries whose name end with a known schema extension.
1251         *
1252         * @param classLoader The class loader to search for resources.
1253         *
1254         * @return URLs of any matching entries.
1255         *
1256         * @throws NullPointerException if {@code classLoader} is {@code null}.
1257         * @throws IOException if reading or parsing fails.
1258         */
1259        private Set<URL> getSchemaResources( final ClassLoader classLoader ) throws IOException
1260        {
1261            if ( classLoader == null )
1262            {
1263                throw new NullPointerException( "classLoader" );
1264            }
1265    
1266            final Set<URL> resources = new HashSet<URL>();
1267            final long t0 = System.currentTimeMillis();
1268            int count = 0;
1269    
1270            for ( final Enumeration<URL> e = classLoader.getResources( "META-INF/MANIFEST.MF" ); e.hasMoreElements(); )
1271            {
1272                count++;
1273                final URL manifestUrl = e.nextElement();
1274                final String externalForm = manifestUrl.toExternalForm();
1275                final String baseUrl = externalForm.substring( 0, externalForm.indexOf( "META-INF" ) );
1276                final InputStream manifestStream = manifestUrl.openStream();
1277                final Manifest mf = new Manifest( manifestStream );
1278                manifestStream.close();
1279    
1280                if ( this.isLoggable( Level.FINE ) )
1281                {
1282                    this.log( Level.FINE, this.getMessage( "processing", new Object[]
1283                        {
1284                            externalForm
1285                        } ), null );
1286    
1287                }
1288    
1289                for ( Map.Entry<String, Attributes> entry : mf.getEntries().entrySet() )
1290                {
1291                    for ( int i = SCHEMA_EXTENSIONS.length - 1; i >= 0; i-- )
1292                    {
1293                        if ( entry.getKey().toLowerCase().endsWith( '.' + SCHEMA_EXTENSIONS[i].toLowerCase() ) )
1294                        {
1295                            final URL schemaUrl = new URL( baseUrl + entry.getKey() );
1296                            resources.add( schemaUrl );
1297    
1298                            if ( this.isLoggable( Level.FINE ) )
1299                            {
1300                                this.log( Level.FINE, this.getMessage( "addingSchemaCandidate", new Object[]
1301                                    {
1302                                        schemaUrl.toExternalForm()
1303                                    } ), null );
1304    
1305                            }
1306                        }
1307                    }
1308                }
1309            }
1310    
1311            if ( this.isLoggable( Level.FINE ) )
1312            {
1313                this.log( Level.FINE, this.getMessage( "classpathReport", new Object[]
1314                    {
1315                        count, "META-INF/MANIFEST.MF", Long.valueOf( System.currentTimeMillis() - t0 )
1316                    } ), null );
1317    
1318            }
1319    
1320            return resources;
1321        }
1322    
1323        private String getMessage( final String key, final Object args )
1324        {
1325            return new MessageFormat(
1326                ResourceBundle.getBundle( DefaultModelManager.class.getName().replace( '.', '/' ), Locale.getDefault() ).
1327                getString( key ) ).format( args );
1328    
1329        }
1330    
1331        // SECTION-END
1332    }