View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.xml.parse;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.Reader;
23  import java.lang.ref.SoftReference;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.Stack;
27  
28  import javax.xml.parsers.DocumentBuilder;
29  import javax.xml.parsers.DocumentBuilderFactory;
30  import javax.xml.parsers.ParserConfigurationException;
31  import javax.xml.validation.Schema;
32  
33  import org.opensaml.xml.Configuration;
34  import org.opensaml.xml.util.LazyMap;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  import org.w3c.dom.DOMImplementation;
38  import org.w3c.dom.Document;
39  import org.xml.sax.EntityResolver;
40  import org.xml.sax.ErrorHandler;
41  import org.xml.sax.InputSource;
42  import org.xml.sax.SAXException;
43  
44  /**
45   * A pool of JAXP 1.3 {@link DocumentBuilder}s.
46   * 
47   * <p>This implementation of {@link ParserPool} does not allow its properties to be modified once
48   * it has been initialized.  For an implementation that does support versioned properties over
49   * time, see {@link BasicParserPool}.</p>
50   * 
51   * <p>This is a pool implementation of the caching factory variety, and as such imposes no upper bound 
52   * on the number of DocumentBuilders allowed to be concurrently checked out and in use. It does however
53   * impose a limit on the size of the internal cache of idle builder instances via the value configured 
54   * via {@link #setMaxPoolSize(int)}.</p>
55   * 
56   * <p>Builders retrieved from this pool may (but are not required to) be returned to the pool with the method
57   * {@link #returnBuilder(DocumentBuilder)}.</p>
58   * 
59   * <p>References to builders are kept by way of {@link SoftReference} so that the garbage collector 
60   * may reap the builders if the system is running out of memory.</p>
61   */
62  public class StaticBasicParserPool implements ParserPool {
63  
64      /** Class logger. */
65      private final Logger log = LoggerFactory.getLogger(StaticBasicParserPool.class);
66      
67      /** Flag to track whether pool is in the initialized state. */
68      private boolean initialized;
69  
70      /** Factory used to create new builders. */
71      private DocumentBuilderFactory builderFactory;
72  
73      /** Cache of document builders. */
74      private Stack<SoftReference<DocumentBuilder>> builderPool;
75  
76      /** Max number of builders allowed in the pool. Default value: 5 */
77      private int maxPoolSize;
78  
79      /** Builder attributes. */
80      private Map<String, Object> builderAttributes;
81  
82      /** Whether the builders are coalescing. Default value: true */
83      private boolean coalescing;
84  
85      /** Whether the builders expand entity references. Default value: true */
86      private boolean expandEntityReferences;
87  
88      /** Builder features. */
89      private Map<String, Boolean> builderFeatures;
90  
91      /** Whether the builders ignore comments. Default value: true */
92      private boolean ignoreComments;
93  
94      /** Whether the builders ignore element content whitespace. Default value: true */
95      private boolean ignoreElementContentWhitespace;
96  
97      /** Whether the builders are namespace aware. Default value: true */
98      private boolean namespaceAware;
99  
100     /** Schema used to validate parsed content. */
101     private Schema schema;
102 
103     /** Whether the builder should validate. Default value: false */
104     private boolean dtdValidating;
105 
106     /** Whether the builders are XInclude aware. Default value: false */
107     private boolean xincludeAware;
108 
109     /** Entity resolver used by builders. */
110     private EntityResolver entityResolver;
111 
112     /** Error handler used by builders. */
113     private ErrorHandler errorHandler;
114 
115     /** Constructor. */
116     public StaticBasicParserPool() {
117         initialized = false;
118         maxPoolSize = 5;
119         builderPool = new Stack<SoftReference<DocumentBuilder>>();
120         builderAttributes = new LazyMap<String, Object>();
121         coalescing = true;
122         expandEntityReferences = true;
123         builderFeatures = new LazyMap<String, Boolean>();
124         ignoreComments = true;
125         ignoreElementContentWhitespace = true;
126         namespaceAware = true;
127         schema = null;
128         dtdValidating = false;
129         xincludeAware = false;
130         errorHandler = new LoggingErrorHandler(log);
131 
132     }
133     
134     /** 
135      * Initialize the pool. 
136      * 
137      * @throws XMLParserException thrown if pool can not be initialized,
138      *        or if it is already initialized
139      * 
140      **/
141     public synchronized void initialize() throws XMLParserException {
142         if (initialized) {
143             throw new XMLParserException("Parser pool was already initialized");
144         }
145         initializeFactory();
146         initialized = true;
147     }
148     
149     /**
150      * Return initialized state. 
151      * 
152      * @return true if pool is initialized, false otherwise
153      */
154     public synchronized boolean isInitialized() {
155         return initialized;
156     }
157 
158     /** {@inheritDoc} */
159     public DocumentBuilder getBuilder() throws XMLParserException {
160         DocumentBuilder builder = null;
161         
162         if (!initialized) {
163             throw new XMLParserException("Parser pool has not been initialized");
164         }
165         
166         synchronized(builderPool) {
167             if (!builderPool.isEmpty()) {
168                 builder = builderPool.pop().get();
169             }
170         }
171         
172         // Will be null if either the stack was empty, or the SoftReference
173         // has been garbage-collected
174         if (builder == null) {
175             builder = createBuilder();
176         }
177 
178         if (builder != null) {
179             return new DocumentBuilderProxy(builder, this);
180         }
181 
182         return null;
183     }
184 
185     /** {@inheritDoc} */
186     public void returnBuilder(DocumentBuilder builder) {
187         if (!(builder instanceof DocumentBuilderProxy)) {
188             return;
189         }
190         
191         DocumentBuilderProxy proxiedBuilder = (DocumentBuilderProxy) builder;
192         if (proxiedBuilder.getOwningPool() != this) {
193             return;
194         }
195         
196         synchronized (proxiedBuilder) {
197             if (proxiedBuilder.isReturned()) {
198                 return;
199             }
200             // Not strictly true in that it may not actually be pushed back
201             // into the cache, depending on builderPool.size() below.  But 
202             // that's ok.  returnBuilder() shouldn't normally be called twice
203             // on the same builder instance anyway, and it also doesn't matter
204             // whether a builder is ever logically returned to the pool.
205             proxiedBuilder.setReturned(true);
206         }
207         
208         DocumentBuilder unwrappedBuilder = proxiedBuilder.getProxiedBuilder();
209         unwrappedBuilder.reset();
210         SoftReference<DocumentBuilder> builderReference = new SoftReference<DocumentBuilder>(unwrappedBuilder);
211         
212         synchronized(builderPool) {
213             if (builderPool.size() < maxPoolSize) {
214                 builderPool.push(builderReference);
215             }
216         }
217     }
218 
219     /** {@inheritDoc} */
220     public Document newDocument() throws XMLParserException {
221         DocumentBuilder builder = getBuilder();
222         Document document = builder.newDocument();
223         returnBuilder(builder);
224         return document;
225     }
226 
227     /** {@inheritDoc} */
228     public Document parse(InputStream input) throws XMLParserException {
229         DocumentBuilder builder = getBuilder();
230         try {
231             Document document = builder.parse(input);
232             return document;
233         } catch (SAXException e) {
234             throw new XMLParserException("Invalid XML", e);
235         } catch (IOException e) {
236             throw new XMLParserException("Unable to read XML from input stream", e);
237         } finally {
238             returnBuilder(builder);
239         }
240     }
241 
242     /** {@inheritDoc} */
243     public Document parse(Reader input) throws XMLParserException {
244         DocumentBuilder builder = getBuilder();
245         try {
246             Document document = builder.parse(new InputSource(input));
247             return document;
248         } catch (SAXException e) {
249             throw new XMLParserException("Invalid XML", e);
250         } catch (IOException e) {
251             throw new XMLParserException("Unable to read XML from input stream", e);
252         } finally {
253             returnBuilder(builder);
254         }
255     }
256 
257     /**
258      * Gets the max number of builders the pool will hold.
259      * 
260      * @return max number of builders the pool will hold
261      */
262     public int getMaxPoolSize() {
263         return maxPoolSize;
264     }
265 
266     /**
267      * Sets the max number of builders the pool will hold.
268      * 
269      * @param newSize max number of builders the pool will hold
270      */
271     public void setMaxPoolSize(int newSize) {
272         checkValidModifyState();
273         maxPoolSize = newSize;
274     }
275 
276     /**
277      * Gets the builder attributes used when creating builders. This collection is unmodifiable.
278      * 
279      * @return builder attributes used when creating builders
280      */
281     public Map<String, Object> getBuilderAttributes() {
282         return Collections.unmodifiableMap(builderAttributes);
283     }
284 
285     /**
286      * Sets the builder attributes used when creating builders.
287      * 
288      * @param newAttributes builder attributes used when creating builders
289      */
290     public void setBuilderAttributes(Map<String, Object> newAttributes) {
291         checkValidModifyState();
292         builderAttributes = newAttributes;
293     }
294 
295     /**
296      * Gets whether the builders are coalescing.
297      * 
298      * @return whether the builders are coalescing
299      */
300     public boolean isCoalescing() {
301         return coalescing;
302     }
303 
304     /**
305      * Sets whether the builders are coalescing.
306      * 
307      * @param isCoalescing whether the builders are coalescing
308      */
309     public void setCoalescing(boolean isCoalescing) {
310         checkValidModifyState();
311         coalescing = isCoalescing;
312     }
313 
314     /**
315      * Gets whether builders expand entity references.
316      * 
317      * @return whether builders expand entity references
318      */
319     public boolean isExpandEntityReferences() {
320         return expandEntityReferences;
321     }
322 
323     /**
324      * Sets whether builders expand entity references.
325      * 
326      * @param expand whether builders expand entity references
327      */
328     public void setExpandEntityReferences(boolean expand) {
329         checkValidModifyState();
330         expandEntityReferences = expand;
331     }
332 
333     /**
334      * Gets the builders' features. This collection is unmodifiable.
335      * 
336      * @return the builders' features
337      */
338     public Map<String, Boolean> getBuilderFeatures() {
339         return Collections.unmodifiableMap(builderFeatures);
340     }
341 
342     /**
343      * Sets the the builders' features.
344      * 
345      * @param newFeatures the builders' features
346      */
347     public void setBuilderFeatures(Map<String, Boolean> newFeatures) {
348         checkValidModifyState();
349         builderFeatures = newFeatures;
350     }
351 
352     /**
353      * Gets whether the builders ignore comments.
354      * 
355      * @return whether the builders ignore comments
356      */
357     public boolean getIgnoreComments() {
358         return ignoreComments;
359     }
360 
361     /**
362      * Sets whether the builders ignore comments.
363      * 
364      * @param ignore The ignoreComments to set.
365      */
366     public void setIgnoreComments(boolean ignore) {
367         checkValidModifyState();
368         ignoreComments = ignore;
369     }
370 
371     /**
372      * Get whether the builders ignore element content whitespace.
373      * 
374      * @return whether the builders ignore element content whitespace
375      */
376     public boolean isIgnoreElementContentWhitespace() {
377         return ignoreElementContentWhitespace;
378     }
379 
380     /**
381      * Sets whether the builders ignore element content whitespace.
382      * 
383      * @param ignore whether the builders ignore element content whitespace
384      */
385     public void setIgnoreElementContentWhitespace(boolean ignore) {
386         checkValidModifyState();
387         ignoreElementContentWhitespace = ignore;
388     }
389 
390     /**
391      * Gets whether the builders are namespace aware.
392      * 
393      * @return whether the builders are namespace aware
394      */
395     public boolean isNamespaceAware() {
396         return namespaceAware;
397     }
398 
399     /**
400      * Sets whether the builders are namespace aware.
401      * 
402      * @param isNamespaceAware whether the builders are namespace aware
403      */
404     public void setNamespaceAware(boolean isNamespaceAware) {
405         checkValidModifyState();
406         namespaceAware = isNamespaceAware;
407     }
408 
409     /** {@inheritDoc} */
410     public Schema getSchema() {
411         return schema;
412     }
413 
414     /** {@inheritDoc} */
415     public synchronized void setSchema(Schema newSchema) {
416         // Note this method is synchronized because it is more than just an atomic assignment.
417         // Don't want inconsistent data in the factory via initializeFactory(), also synchronized.
418         checkValidModifyState();
419         schema = newSchema;
420         if (schema != null) {
421             setNamespaceAware(true);
422             builderAttributes.remove("http://java.sun.com/xml/jaxp/properties/schemaSource");
423             builderAttributes.remove("http://java.sun.com/xml/jaxp/properties/schemaLanguage");
424         }
425     }
426 
427     /**
428      * Gets whether the builders are validating.
429      * 
430      * @return whether the builders are validating
431      */
432     public boolean isDTDValidating() {
433         return dtdValidating;
434     }
435 
436     /**
437      * Sets whether the builders are validating.
438      * 
439      * @param isValidating whether the builders are validating
440      */
441     public void setDTDValidating(boolean isValidating) {
442         checkValidModifyState();
443         dtdValidating = isValidating;
444     }
445 
446     /**
447      * Gets whether the builders are XInclude aware.
448      * 
449      * @return whether the builders are XInclude aware
450      */
451     public boolean isXincludeAware() {
452         return xincludeAware;
453     }
454 
455     /**
456      * Sets whether the builders are XInclude aware.
457      * 
458      * @param isXIncludeAware whether the builders are XInclude aware
459      */
460     public void setXincludeAware(boolean isXIncludeAware) {
461         checkValidModifyState();
462         xincludeAware = isXIncludeAware;
463     }
464 
465     /**
466      * Gets the size of the current pool storage.
467      * 
468      * @return current pool storage size
469      */
470     protected int getPoolSize() {
471         return builderPool.size();
472     }
473     
474     /**
475      * Check that pool is in a valid state to be modified.
476      */
477     protected void checkValidModifyState() {
478         if (initialized) {
479             throw new IllegalStateException("Pool is already initialized, property changes not allowed");
480         }
481     }
482 
483     /**
484      * Initializes the pool with a new set of configuration options.
485      * 
486      * @throws XMLParserException thrown if there is a problem initialzing the pool
487      */
488     protected synchronized void initializeFactory() throws XMLParserException {
489         DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance();
490         setAttributes(newFactory, builderAttributes);
491         setFeatures(newFactory, builderFeatures);
492         newFactory.setCoalescing(coalescing);
493         newFactory.setExpandEntityReferences(expandEntityReferences);
494         newFactory.setIgnoringComments(ignoreComments);
495         newFactory.setIgnoringElementContentWhitespace(ignoreElementContentWhitespace);
496         newFactory.setNamespaceAware(namespaceAware);
497         newFactory.setSchema(schema);
498         newFactory.setValidating(dtdValidating);
499         newFactory.setXIncludeAware(xincludeAware);
500         builderFactory = newFactory;
501     }
502     
503     /**
504      * Sets document builder attributes. If an attribute is not supported it is ignored.
505      * 
506      * @param factory document builder factory upon which the attribute will be set
507      * @param attributes the set of attributes to be set
508      */
509     protected void setAttributes(DocumentBuilderFactory factory, Map<String, Object> attributes) {
510         if (attributes == null || attributes.isEmpty()) {
511             return;
512         }
513 
514         for (Map.Entry<String, Object> attribute : attributes.entrySet()) {
515             try {
516                 log.debug("Setting DocumentBuilderFactory attribute '{}'", attribute.getKey());
517                 factory.setAttribute(attribute.getKey(), attribute.getValue());
518             } catch (IllegalArgumentException e) {
519                 log.warn("DocumentBuilderFactory attribute '{}' is not supported", attribute.getKey());
520             }
521         }
522     }
523 
524     /**
525      * Sets document builder features. If an features is not supported it is ignored.
526      * 
527      * @param factory document builder factory upon which the attribute will be set
528      * @param features the set of features to be set
529      */
530     protected void setFeatures(DocumentBuilderFactory factory, Map<String, Boolean> features) {
531         if (features == null || features.isEmpty()) {
532             return;
533         }
534 
535         for (Map.Entry<String, Boolean> feature : features.entrySet()) {
536             try {
537                 log.debug("Setting DocumentBuilderFactory attribute '{}'", feature.getKey());
538                 factory.setFeature(feature.getKey(), feature.getValue());
539             } catch (ParserConfigurationException e) {
540                 log.warn("DocumentBuilderFactory feature '{}' is not supported", feature.getKey());
541             }
542         }
543     }
544 
545     /**
546      * Creates a new document builder.
547      * 
548      * @return newly created document builder
549      * 
550      * @throws XMLParserException thrown if their is a configuration error with the builder factory
551      */
552     protected DocumentBuilder createBuilder() throws XMLParserException {
553         try {
554             DocumentBuilder builder = builderFactory.newDocumentBuilder();
555 
556             if (entityResolver != null) {
557                 builder.setEntityResolver(entityResolver);
558             }
559 
560             if (errorHandler != null) {
561                 builder.setErrorHandler(errorHandler);
562             }
563 
564             return builder;
565         } catch (ParserConfigurationException e) {
566             log.error("Unable to create new document builder", e);
567             throw new XMLParserException("Unable to create new document builder", e);
568         }
569     }
570 
571     /**
572      * A proxy that prevents the manages document builders retrieved from the parser pool.
573      */
574     protected class DocumentBuilderProxy extends DocumentBuilder {
575 
576         /** Builder being proxied. */
577         private DocumentBuilder builder;
578 
579         /** Pool that owns this parser. */
580         private ParserPool owningPool;
581 
582         /** Track accounting state of whether this builder has been returned to the owning pool. */
583         private boolean returned;
584 
585         /**
586          * Constructor.
587          * 
588          * @param target document builder to proxy
589          * @param owner the owning pool
590          */
591         public DocumentBuilderProxy(DocumentBuilder target, StaticBasicParserPool owner) {
592             owningPool = owner;
593             builder = target;
594             returned = false;
595         }
596 
597         /** {@inheritDoc} */
598         public DOMImplementation getDOMImplementation() {
599             checkValidState();
600             return builder.getDOMImplementation();
601         }
602 
603         /** {@inheritDoc} */
604         public Schema getSchema() {
605             checkValidState();
606             return builder.getSchema();
607         }
608 
609         /** {@inheritDoc} */
610         public boolean isNamespaceAware() {
611             checkValidState();
612             return builder.isNamespaceAware();
613         }
614 
615         /** {@inheritDoc} */
616         public boolean isValidating() {
617             checkValidState();
618             return builder.isValidating();
619         }
620 
621         /** {@inheritDoc} */
622         public boolean isXIncludeAware() {
623             checkValidState();
624             return builder.isXIncludeAware();
625         }
626 
627         /** {@inheritDoc} */
628         public Document newDocument() {
629             checkValidState();
630             return builder.newDocument();
631         }
632 
633         /** {@inheritDoc} */
634         public Document parse(File f) throws SAXException, IOException {
635             checkValidState();
636             return builder.parse(f);
637         }
638 
639         /** {@inheritDoc} */
640         public Document parse(InputSource is) throws SAXException, IOException {
641             checkValidState();
642             return builder.parse(is);
643         }
644 
645         /** {@inheritDoc} */
646         public Document parse(InputStream is) throws SAXException, IOException {
647             checkValidState();
648             return builder.parse(is);
649         }
650 
651         /** {@inheritDoc} */
652         public Document parse(InputStream is, String systemId) throws SAXException, IOException {
653             checkValidState();
654             return builder.parse(is, systemId);
655         }
656 
657         /** {@inheritDoc} */
658         public Document parse(String uri) throws SAXException, IOException {
659             checkValidState();
660             return builder.parse(uri);
661         }
662 
663         /** {@inheritDoc} */
664         public void reset() {
665             // ignore, entity resolver and error handler can't be changed
666         }
667 
668         /** {@inheritDoc} */
669         public void setEntityResolver(EntityResolver er) {
670             checkValidState();
671             return;
672         }
673 
674         /** {@inheritDoc} */
675         public void setErrorHandler(ErrorHandler eh) {
676             checkValidState();
677             return;
678         }
679 
680         /**
681          * Gets the pool that owns this parser.
682          * 
683          * @return pool that owns this parser
684          */
685         protected ParserPool getOwningPool() {
686             return owningPool;
687         }
688         
689         /**
690          * Gets the proxied document builder.
691          * 
692          * @return proxied document builder
693          */
694         protected DocumentBuilder getProxiedBuilder() {
695             return builder;
696         }
697         
698         /**
699          * Check accounting state as to whether this parser has been returned to the
700          * owning pool.
701          * 
702          * @return true if parser has been returned to the owning pool, otherwise false
703          */
704         protected boolean isReturned() {
705             return returned;
706         }
707         
708         /**
709          * Set accounting state as to whether this parser has been returned to the
710          * owning pool.
711          * 
712          * @param isReturned set true to indicate that parser has been returned to the owning pool
713          */
714         protected void setReturned(boolean isReturned) {
715            this.returned = isReturned; 
716         }
717         
718         /**
719          * Check whether the parser is in a valid and usable state, and if not, throw a runtime exception.
720          * 
721          * @throws IllegalStateException thrown if the parser is in a state such that it can not be used
722          */
723         protected void checkValidState() throws IllegalStateException {
724             if (isReturned()) {
725                 throw new IllegalStateException("DocumentBuilderProxy has already been returned to its owning pool");
726             }
727         }
728 
729         /** {@inheritDoc} */
730         protected void finalize() throws Throwable {
731             super.finalize();
732             owningPool.returnBuilder(this);
733         }
734     }
735 }