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