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