1 | /* |
2 | * Copyright (c) 2009 The JOMC Project |
3 | * Copyright (c) 2005 Christian Schulte <cs@jomc.org> |
4 | * All rights reserved. |
5 | * |
6 | * Redistribution and use in source and binary forms, with or without |
7 | * modification, are permitted provided that the following conditions |
8 | * are met: |
9 | * |
10 | * o Redistributions of source code must retain the above copyright |
11 | * notice, this list of conditions and the following disclaimer. |
12 | * |
13 | * o Redistributions in binary form must reproduce the above copyright |
14 | * notice, this list of conditions and the following disclaimer in |
15 | * the documentation and/or other materials provided with the |
16 | * distribution. |
17 | * |
18 | * THIS SOFTWARE IS PROVIDED BY THE JOMC PROJECT AND CONTRIBUTORS "AS IS" |
19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
20 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
21 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE JOMC PROJECT OR |
22 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
23 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
24 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
25 | * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
26 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
27 | * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
28 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
29 | * |
30 | * $Id: DefaultModelContext.java 1289 2010-01-15 08:54:31Z schulte2005 $ |
31 | * |
32 | */ |
33 | package org.jomc.model; |
34 | |
35 | import java.io.IOException; |
36 | import java.io.InputStream; |
37 | import java.io.Reader; |
38 | import java.lang.ref.Reference; |
39 | import java.lang.ref.SoftReference; |
40 | import java.net.URI; |
41 | import java.net.URISyntaxException; |
42 | import java.net.URL; |
43 | import java.text.MessageFormat; |
44 | import java.util.ArrayList; |
45 | import java.util.Enumeration; |
46 | import java.util.HashSet; |
47 | import java.util.Iterator; |
48 | import java.util.List; |
49 | import java.util.Locale; |
50 | import java.util.Map; |
51 | import java.util.ResourceBundle; |
52 | import java.util.Set; |
53 | import java.util.jar.Attributes; |
54 | import java.util.jar.Manifest; |
55 | import java.util.logging.Level; |
56 | import javax.xml.XMLConstants; |
57 | import javax.xml.bind.JAXBContext; |
58 | import javax.xml.bind.JAXBException; |
59 | import javax.xml.bind.Marshaller; |
60 | import javax.xml.bind.Unmarshaller; |
61 | import javax.xml.transform.Source; |
62 | import javax.xml.transform.sax.SAXSource; |
63 | import javax.xml.validation.Schema; |
64 | import javax.xml.validation.SchemaFactory; |
65 | import org.jomc.model.bootstrap.BootstrapContext; |
66 | import org.jomc.model.bootstrap.BootstrapException; |
67 | import org.jomc.model.bootstrap.Schemas; |
68 | import org.w3c.dom.ls.LSInput; |
69 | import org.w3c.dom.ls.LSResourceResolver; |
70 | import org.xml.sax.EntityResolver; |
71 | import org.xml.sax.InputSource; |
72 | import org.xml.sax.SAXException; |
73 | import org.xml.sax.helpers.DefaultHandler; |
74 | |
75 | /** |
76 | * Default {@code ModelContext} implementation. |
77 | * |
78 | * @author <a href="mailto:cs@jomc.org">Christian Schulte</a> |
79 | * @version $Id: DefaultModelContext.java 1289 2010-01-15 08:54:31Z schulte2005 $ |
80 | * @see ModelContext#createModelContext(java.lang.ClassLoader) |
81 | */ |
82 | public class DefaultModelContext extends ModelContext |
83 | { |
84 | |
85 | /** Supported schema name extensions. */ |
86 | private static final String[] SCHEMA_EXTENSIONS = new String[] |
87 | { |
88 | "xsd" |
89 | }; |
90 | |
91 | /** Cached {@code Schemas}. */ |
92 | private Reference<Schemas> cachedSchemas = new SoftReference<Schemas>( null ); |
93 | |
94 | /** Cached schema resources. */ |
95 | private Reference<Set<URI>> cachedSchemaResources = new SoftReference<Set<URI>>( null ); |
96 | |
97 | /** |
98 | * Creates a new {@code DefaultModelContext} instance taking a class loader. |
99 | * |
100 | * @param classLoader The class loader of the context. |
101 | */ |
102 | public DefaultModelContext( final ClassLoader classLoader ) |
103 | { |
104 | super( classLoader ); |
105 | } |
106 | |
107 | @Override |
108 | public EntityResolver createEntityResolver() throws ModelException |
109 | { |
110 | return new DefaultHandler() |
111 | { |
112 | |
113 | @Override |
114 | public InputSource resolveEntity( final String publicId, final String systemId ) |
115 | throws SAXException, IOException |
116 | { |
117 | if ( systemId == null ) |
118 | { |
119 | throw new NullPointerException( "systemId" ); |
120 | } |
121 | |
122 | InputSource schemaSource = null; |
123 | |
124 | try |
125 | { |
126 | org.jomc.model.bootstrap.Schema s = null; |
127 | final Schemas classpathSchemas = getSchemas(); |
128 | |
129 | if ( publicId != null ) |
130 | { |
131 | s = classpathSchemas.getSchemaByPublicId( publicId ); |
132 | } |
133 | if ( s == null ) |
134 | { |
135 | s = classpathSchemas.getSchemaBySystemId( systemId ); |
136 | } |
137 | |
138 | if ( s != null ) |
139 | { |
140 | schemaSource = new InputSource(); |
141 | schemaSource.setPublicId( s.getPublicId() != null ? s.getPublicId() : publicId ); |
142 | schemaSource.setSystemId( s.getSystemId() ); |
143 | |
144 | if ( s.getClasspathId() != null ) |
145 | { |
146 | final URL resource = findResource( s.getClasspathId() ); |
147 | |
148 | if ( resource != null ) |
149 | { |
150 | schemaSource.setSystemId( resource.toExternalForm() ); |
151 | } |
152 | else |
153 | { |
154 | if ( isLoggable( Level.WARNING ) ) |
155 | { |
156 | log( Level.WARNING, getMessage( "resourceNotFound", new Object[] |
157 | { |
158 | s.getClasspathId() |
159 | } ), null ); |
160 | |
161 | } |
162 | } |
163 | } |
164 | |
165 | if ( isLoggable( Level.FINE ) ) |
166 | { |
167 | log( Level.FINE, getMessage( "resolutionInfo", new Object[] |
168 | { |
169 | publicId + ", " + systemId, |
170 | schemaSource.getPublicId() + ", " + schemaSource.getSystemId() |
171 | } ), null ); |
172 | |
173 | } |
174 | } |
175 | |
176 | if ( schemaSource == null ) |
177 | { |
178 | final URI systemUri = new URI( systemId ); |
179 | String schemaName = systemUri.getPath(); |
180 | if ( schemaName != null ) |
181 | { |
182 | final int lastIndexOfSlash = schemaName.lastIndexOf( '/' ); |
183 | if ( lastIndexOfSlash != -1 && lastIndexOfSlash < schemaName.length() ) |
184 | { |
185 | schemaName = schemaName.substring( lastIndexOfSlash + 1 ); |
186 | } |
187 | |
188 | for ( URI uri : getSchemaResources() ) |
189 | { |
190 | if ( uri.getPath().endsWith( schemaName ) ) |
191 | { |
192 | schemaSource = new InputSource(); |
193 | schemaSource.setPublicId( publicId ); |
194 | schemaSource.setSystemId( uri.toASCIIString() ); |
195 | |
196 | if ( isLoggable( Level.FINE ) ) |
197 | { |
198 | log( Level.FINE, getMessage( "resolutionInfo", new Object[] |
199 | { |
200 | systemUri.toASCIIString(), |
201 | schemaSource.getSystemId() |
202 | } ), null ); |
203 | |
204 | } |
205 | |
206 | break; |
207 | } |
208 | } |
209 | } |
210 | else |
211 | { |
212 | if ( isLoggable( Level.WARNING ) ) |
213 | { |
214 | log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[] |
215 | { |
216 | systemId, systemUri.toASCIIString() |
217 | } ), null ); |
218 | |
219 | } |
220 | |
221 | schemaSource = null; |
222 | } |
223 | } |
224 | } |
225 | catch ( final URISyntaxException e ) |
226 | { |
227 | if ( isLoggable( Level.WARNING ) ) |
228 | { |
229 | log( Level.WARNING, getMessage( "unsupportedSystemIdUri", new Object[] |
230 | { |
231 | systemId, e.getMessage() |
232 | } ), null ); |
233 | |
234 | } |
235 | |
236 | schemaSource = null; |
237 | } |
238 | catch ( final BootstrapException e ) |
239 | { |
240 | throw (IOException) new IOException( getMessage( "failedResolvingSchemas", null ) ).initCause( e ); |
241 | } |
242 | catch ( final ModelException e ) |
243 | { |
244 | throw (IOException) new IOException( getMessage( "failedResolving", new Object[] |
245 | { |
246 | publicId, systemId, e.getMessage() |
247 | } ) ).initCause( e ); |
248 | |
249 | } |
250 | |
251 | return schemaSource; |
252 | } |
253 | |
254 | }; |
255 | } |
256 | |
257 | @Override |
258 | public LSResourceResolver createResourceResolver() throws ModelException |
259 | { |
260 | return new LSResourceResolver() |
261 | { |
262 | |
263 | public LSInput resolveResource( final String type, final String namespaceURI, final String publicId, |
264 | final String systemId, final String baseURI ) |
265 | { |
266 | final String resolvePublicId = namespaceURI == null ? publicId : namespaceURI; |
267 | final String resolveSystemId = systemId == null ? "" : systemId; |
268 | |
269 | try |
270 | { |
271 | if ( XMLConstants.W3C_XML_SCHEMA_NS_URI.equals( type ) ) |
272 | { |
273 | final InputSource schemaSource = |
274 | createEntityResolver().resolveEntity( resolvePublicId, resolveSystemId ); |
275 | |
276 | if ( schemaSource != null ) |
277 | { |
278 | return new LSInput() |
279 | { |
280 | |
281 | public Reader getCharacterStream() |
282 | { |
283 | return schemaSource.getCharacterStream(); |
284 | } |
285 | |
286 | public void setCharacterStream( final Reader characterStream ) |
287 | { |
288 | throw new UnsupportedOperationException(); |
289 | } |
290 | |
291 | public InputStream getByteStream() |
292 | { |
293 | return schemaSource.getByteStream(); |
294 | } |
295 | |
296 | public void setByteStream( final InputStream byteStream ) |
297 | { |
298 | throw new UnsupportedOperationException(); |
299 | } |
300 | |
301 | public String getStringData() |
302 | { |
303 | return null; |
304 | } |
305 | |
306 | public void setStringData( final String stringData ) |
307 | { |
308 | throw new UnsupportedOperationException(); |
309 | } |
310 | |
311 | public String getSystemId() |
312 | { |
313 | return schemaSource.getSystemId(); |
314 | } |
315 | |
316 | public void setSystemId( final String systemId ) |
317 | { |
318 | throw new UnsupportedOperationException(); |
319 | } |
320 | |
321 | public String getPublicId() |
322 | { |
323 | return schemaSource.getPublicId(); |
324 | } |
325 | |
326 | public void setPublicId( final String publicId ) |
327 | { |
328 | throw new UnsupportedOperationException(); |
329 | } |
330 | |
331 | public String getBaseURI() |
332 | { |
333 | return baseURI; |
334 | } |
335 | |
336 | public void setBaseURI( final String baseURI ) |
337 | { |
338 | throw new UnsupportedOperationException(); |
339 | } |
340 | |
341 | public String getEncoding() |
342 | { |
343 | return schemaSource.getEncoding(); |
344 | } |
345 | |
346 | public void setEncoding( final String encoding ) |
347 | { |
348 | throw new UnsupportedOperationException(); |
349 | } |
350 | |
351 | public boolean getCertifiedText() |
352 | { |
353 | return false; |
354 | } |
355 | |
356 | public void setCertifiedText( final boolean certifiedText ) |
357 | { |
358 | throw new UnsupportedOperationException(); |
359 | } |
360 | |
361 | }; |
362 | } |
363 | |
364 | } |
365 | else if ( isLoggable( Level.WARNING ) ) |
366 | { |
367 | log( Level.WARNING, getMessage( "unsupportedResourceType", new Object[] |
368 | { |
369 | type |
370 | } ), null ); |
371 | |
372 | } |
373 | } |
374 | catch ( final SAXException e ) |
375 | { |
376 | if ( isLoggable( Level.SEVERE ) ) |
377 | { |
378 | log( Level.SEVERE, getMessage( "failedResolving", new Object[] |
379 | { |
380 | resolvePublicId, resolveSystemId, e.getMessage() |
381 | } ), e ); |
382 | |
383 | } |
384 | } |
385 | catch ( final IOException e ) |
386 | { |
387 | if ( isLoggable( Level.SEVERE ) ) |
388 | { |
389 | log( Level.SEVERE, getMessage( "failedResolving", new Object[] |
390 | { |
391 | resolvePublicId, resolveSystemId, e.getMessage() |
392 | } ), e ); |
393 | |
394 | } |
395 | } |
396 | catch ( final ModelException e ) |
397 | { |
398 | if ( isLoggable( Level.SEVERE ) ) |
399 | { |
400 | log( Level.SEVERE, getMessage( "failedResolving", new Object[] |
401 | { |
402 | resolvePublicId, resolveSystemId, e.getMessage() |
403 | } ), e ); |
404 | |
405 | } |
406 | } |
407 | |
408 | return null; |
409 | } |
410 | |
411 | }; |
412 | |
413 | } |
414 | |
415 | @Override |
416 | public Schema createSchema() throws ModelException |
417 | { |
418 | try |
419 | { |
420 | final SchemaFactory f = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ); |
421 | final Schemas schemas = this.getSchemas(); |
422 | final List<Source> sources = new ArrayList<Source>( schemas.getSchema().size() ); |
423 | final EntityResolver entityResolver = this.createEntityResolver(); |
424 | |
425 | for ( org.jomc.model.bootstrap.Schema s : schemas.getSchema() ) |
426 | { |
427 | final InputSource inputSource = entityResolver.resolveEntity( s.getPublicId(), s.getSystemId() ); |
428 | |
429 | if ( inputSource != null ) |
430 | { |
431 | sources.add( new SAXSource( inputSource ) ); |
432 | } |
433 | } |
434 | |
435 | if ( sources.isEmpty() ) |
436 | { |
437 | throw new ModelException( this.getMessage( "missingSchemas", null ) ); |
438 | } |
439 | |
440 | return f.newSchema( sources.toArray( new Source[ sources.size() ] ) ); |
441 | } |
442 | catch ( final BootstrapException e ) |
443 | { |
444 | throw new ModelException( e ); |
445 | } |
446 | catch ( final IOException e ) |
447 | { |
448 | throw new ModelException( e ); |
449 | } |
450 | catch ( final SAXException e ) |
451 | { |
452 | throw new ModelException( e ); |
453 | } |
454 | } |
455 | |
456 | @Override |
457 | public JAXBContext createContext() throws ModelException |
458 | { |
459 | try |
460 | { |
461 | final StringBuilder packageNames = new StringBuilder(); |
462 | |
463 | for ( final Iterator<org.jomc.model.bootstrap.Schema> s = this.getSchemas().getSchema().iterator(); |
464 | s.hasNext(); ) |
465 | { |
466 | final org.jomc.model.bootstrap.Schema schema = s.next(); |
467 | if ( schema.getContextId() != null ) |
468 | { |
469 | packageNames.append( ':' ).append( schema.getContextId() ); |
470 | if ( this.isLoggable( Level.FINE ) ) |
471 | { |
472 | this.log( Level.FINE, this.getMessage( "foundContext", new Object[] |
473 | { |
474 | schema.getContextId() |
475 | } ), null ); |
476 | |
477 | } |
478 | } |
479 | } |
480 | |
481 | if ( packageNames.length() == 0 ) |
482 | { |
483 | throw new ModelException( this.getMessage( "missingSchemas", null ) ); |
484 | } |
485 | |
486 | return JAXBContext.newInstance( packageNames.toString().substring( 1 ), this.getClassLoader() ); |
487 | } |
488 | catch ( final BootstrapException e ) |
489 | { |
490 | throw new ModelException( e ); |
491 | } |
492 | catch ( final JAXBException e ) |
493 | { |
494 | throw new ModelException( e ); |
495 | } |
496 | } |
497 | |
498 | @Override |
499 | public Marshaller createMarshaller() throws ModelException |
500 | { |
501 | try |
502 | { |
503 | final StringBuilder packageNames = new StringBuilder(); |
504 | final StringBuilder schemaLocation = new StringBuilder(); |
505 | |
506 | for ( final Iterator<org.jomc.model.bootstrap.Schema> s = this.getSchemas().getSchema().iterator(); |
507 | s.hasNext(); ) |
508 | { |
509 | final org.jomc.model.bootstrap.Schema schema = s.next(); |
510 | if ( schema.getContextId() != null ) |
511 | { |
512 | packageNames.append( ':' ).append( schema.getContextId() ); |
513 | } |
514 | if ( schema.getPublicId() != null && schema.getSystemId() != null ) |
515 | { |
516 | schemaLocation.append( ' ' ).append( schema.getPublicId() ).append( ' ' ). |
517 | append( schema.getSystemId() ); |
518 | |
519 | } |
520 | } |
521 | |
522 | if ( packageNames.length() == 0 ) |
523 | { |
524 | throw new ModelException( this.getMessage( "missingSchemas", null ) ); |
525 | } |
526 | |
527 | final Marshaller m = |
528 | JAXBContext.newInstance( packageNames.toString().substring( 1 ), this.getClassLoader() ). |
529 | createMarshaller(); |
530 | |
531 | if ( schemaLocation.length() != 0 ) |
532 | { |
533 | m.setProperty( Marshaller.JAXB_SCHEMA_LOCATION, schemaLocation.toString().substring( 1 ) ); |
534 | } |
535 | |
536 | return m; |
537 | } |
538 | catch ( final BootstrapException e ) |
539 | { |
540 | throw new ModelException( e ); |
541 | } |
542 | catch ( final JAXBException e ) |
543 | { |
544 | throw new ModelException( e ); |
545 | } |
546 | } |
547 | |
548 | @Override |
549 | public Unmarshaller createUnmarshaller() throws ModelException |
550 | { |
551 | try |
552 | { |
553 | return this.createContext().createUnmarshaller(); |
554 | } |
555 | catch ( final JAXBException e ) |
556 | { |
557 | throw new ModelException( e ); |
558 | } |
559 | } |
560 | |
561 | /** |
562 | * Gets the schemas of the instance. |
563 | * |
564 | * @return The schemas of the instance. |
565 | * |
566 | * @throws BootstrapException if getting the schemas fails. |
567 | */ |
568 | private Schemas getSchemas() throws BootstrapException |
569 | { |
570 | Schemas schemas = this.cachedSchemas.get(); |
571 | |
572 | if ( schemas == null ) |
573 | { |
574 | schemas = BootstrapContext.createBootstrapContext( this.getClassLoader() ).findSchemas(); |
575 | |
576 | if ( schemas != null && this.isLoggable( Level.CONFIG ) ) |
577 | { |
578 | for ( org.jomc.model.bootstrap.Schema s : schemas.getSchema() ) |
579 | { |
580 | this.log( Level.CONFIG, this.getMessage( "foundSchema", new Object[] |
581 | { |
582 | s.getPublicId(), s.getSystemId(), s.getContextId(), s.getClasspathId() |
583 | } ), null ); |
584 | |
585 | } |
586 | } |
587 | |
588 | this.cachedSchemas = new SoftReference( schemas ); |
589 | } |
590 | |
591 | return schemas; |
592 | } |
593 | |
594 | /** |
595 | * Searches the context for {@code META-INF/MANIFEST.MF} resources and returns a set of URIs of entries whose name |
596 | * end with a known schema extension. |
597 | * |
598 | * @return Set of URIs of any matching entries. |
599 | * |
600 | * @throws IOException if reading fails. |
601 | * @throws URISyntaxException if parsing fails. |
602 | */ |
603 | private Set<URI> getSchemaResources() throws IOException, URISyntaxException |
604 | { |
605 | Set<URI> resources = this.cachedSchemaResources.get(); |
606 | |
607 | if ( resources == null ) |
608 | { |
609 | resources = new HashSet<URI>(); |
610 | final long t0 = System.currentTimeMillis(); |
611 | int count = 0; |
612 | |
613 | for ( final Enumeration<URL> e = this.getClassLoader().getResources( "META-INF/MANIFEST.MF" ); |
614 | e.hasMoreElements(); ) |
615 | { |
616 | count++; |
617 | final URL manifestUrl = e.nextElement(); |
618 | final String externalForm = manifestUrl.toExternalForm(); |
619 | final String baseUrl = externalForm.substring( 0, externalForm.indexOf( "META-INF" ) ); |
620 | final InputStream manifestStream = manifestUrl.openStream(); |
621 | final Manifest mf = new Manifest( manifestStream ); |
622 | manifestStream.close(); |
623 | |
624 | if ( this.isLoggable( Level.FINE ) ) |
625 | { |
626 | this.log( Level.FINE, this.getMessage( "processing", new Object[] |
627 | { |
628 | externalForm |
629 | } ), null ); |
630 | |
631 | } |
632 | |
633 | for ( Map.Entry<String, Attributes> entry : mf.getEntries().entrySet() ) |
634 | { |
635 | for ( int i = SCHEMA_EXTENSIONS.length - 1; i >= 0; i-- ) |
636 | { |
637 | if ( entry.getKey().toLowerCase().endsWith( '.' + SCHEMA_EXTENSIONS[i].toLowerCase() ) ) |
638 | { |
639 | final URL schemaUrl = new URL( baseUrl + entry.getKey() ); |
640 | resources.add( schemaUrl.toURI() ); |
641 | |
642 | if ( this.isLoggable( Level.FINE ) ) |
643 | { |
644 | this.log( Level.FINE, this.getMessage( "foundSchemaCandidate", new Object[] |
645 | { |
646 | schemaUrl.toExternalForm() |
647 | } ), null ); |
648 | |
649 | } |
650 | } |
651 | } |
652 | } |
653 | } |
654 | |
655 | if ( this.isLoggable( Level.FINE ) ) |
656 | { |
657 | this.log( Level.FINE, this.getMessage( "contextReport", new Object[] |
658 | { |
659 | count, "META-INF/MANIFEST.MF", Long.valueOf( System.currentTimeMillis() - t0 ) |
660 | } ), null ); |
661 | |
662 | } |
663 | |
664 | this.cachedSchemaResources = new SoftReference( resources ); |
665 | } |
666 | |
667 | return resources; |
668 | } |
669 | |
670 | private String getMessage( final String key, final Object args ) |
671 | { |
672 | return new MessageFormat( ResourceBundle.getBundle( DefaultModelContext.class.getName().replace( '.', '/' ), |
673 | Locale.getDefault() ).getString( key ) ).format( args ); |
674 | |
675 | } |
676 | |
677 | } |