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