View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.Reader;
24  import java.io.Writer;
25  import java.net.URL;
26  import java.net.URLConnection;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  
34  import javax.xml.parsers.DocumentBuilder;
35  import javax.xml.parsers.DocumentBuilderFactory;
36  import javax.xml.parsers.ParserConfigurationException;
37  import javax.xml.transform.OutputKeys;
38  import javax.xml.transform.Result;
39  import javax.xml.transform.Source;
40  import javax.xml.transform.Transformer;
41  import javax.xml.transform.TransformerException;
42  import javax.xml.transform.TransformerFactory;
43  import javax.xml.transform.TransformerFactoryConfigurationError;
44  import javax.xml.transform.dom.DOMSource;
45  import javax.xml.transform.stream.StreamResult;
46  
47  import org.apache.commons.configuration.tree.ConfigurationNode;
48  import org.w3c.dom.Attr;
49  import org.w3c.dom.CDATASection;
50  import org.w3c.dom.DOMException;
51  import org.w3c.dom.Document;
52  import org.w3c.dom.Element;
53  import org.w3c.dom.NamedNodeMap;
54  import org.w3c.dom.NodeList;
55  import org.w3c.dom.Text;
56  import org.xml.sax.EntityResolver;
57  import org.xml.sax.InputSource;
58  import org.xml.sax.SAXException;
59  import org.xml.sax.SAXParseException;
60  import org.xml.sax.helpers.DefaultHandler;
61  
62  /***
63   * <p>A specialized hierarchical configuration class that is able to parse XML
64   * documents.</p>
65   *
66   * <p>The parsed document will be stored keeping its structure. The class also
67   * tries to preserve as much information from the loaded XML document as
68   * possible, including comments and processing instructions. These will be
69   * contained in documents created by the <code>save()</code> methods, too.</p>
70   *
71   * <p>Like other file based configuration classes this class maintains the name
72   * and path to the loaded configuration file. These properties can be altered
73   * using several setter methods, but they are not modified by <code>save()</code>
74   * and <code>load()</code> methods. If XML documents contain relative paths to
75   * other documents (e.g. to a DTD), these references are resolved based on the
76   * path set for this configuration.</p>
77   *
78   * <p>By inheriting from <code>{@link AbstractConfiguration}</code> this class
79   * provides some extended functionality, e.g. interpolation of property values.
80   * Like in <code>{@link PropertiesConfiguration}</code> property values can
81   * contain delimiter characters (the comma ',' per default) and are then split
82   * into multiple values. This works for XML attributes and text content of
83   * elements as well. The delimiter can be escaped by a backslash. As an example
84   * consider the following XML fragment:</p>
85   *
86   * <p>
87   * <pre>
88   * &lt;config&gt;
89   *   &lt;array&gt;10,20,30,40&lt;/array&gt;
90   *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
91   *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
92   * &lt;/config&gt;
93   * </pre>
94   * </p>
95   * <p>Here the content of the <code>array</code> element will be split at
96   * the commas, so the <code>array</code> key will be assigned 4 values. In the
97   * <code>scalar</code> property and the <code>text</code> attribute of the
98   * <code>cite</code> element the comma is escaped, so that no splitting is
99   * performed.</p>
100  *
101  * <p>The configuration API allows setting multiple values for a single attribute,
102  * e.g. something like the following is legal (assuming that the default
103  * expression engine is used):
104  * <pre>
105  * XMLConfiguration config = new XMLConfiguration();
106  * config.addProperty("test.dir[@name]", "C://Temp//");
107  * config.addProperty("test.dir[@name]", "D://Data//");
108  * </pre></p>
109  *
110  * <p>Because in XML such a constellation is not directly supported (an attribute
111  * can appear only once for a single element), the values are concatenated to a
112  * single value. If delimiter parsing is enabled (refer to the
113  * <code>{@link #setDelimiterParsingDisabled(boolean)}</code> method), the
114  * current list delimiter character will be used as separator. Otherwise the
115  * pipe symbol ("|") will be used for this purpose. No matter which character is
116  * used as delimiter, it can always be escaped with a backslash. A backslash
117  * itself can also be escaped with another backslash. Consider the following
118  * example fragment from a configuration file:
119  * <pre>
120  * &lt;directories names="C:\Temp//|D:\Data\"/&gt;
121  * </pre>
122  * Here the backslash after Temp is escaped. This is necessary because it
123  * would escape the list delimiter (the pipe symbol assuming that list delimiter
124  * parsing is disabled) otherwise. So this attribute would have two values.</p>
125  *
126  * <p>Note: You should ensure that the <em>delimiter parsing disabled</em>
127  * property is always consistent when you load and save a configuration file.
128  * Otherwise the values of properties can become corrupted.</p>
129  *
130  * <p><code>XMLConfiguration</code> implements the <code>{@link FileConfiguration}</code>
131  * interface and thus provides full support for loading XML documents from
132  * different sources like files, URLs, or streams. A full description of these
133  * features can be found in the documentation of
134  * <code>{@link AbstractFileConfiguration}</code>.</p>
135  *
136  * <p><em>Note:</em>Configuration objects of this type can be read concurrently
137  * by multiple threads. However if one of these threads modifies the object,
138  * synchronization has to be performed manually.</p>
139  *
140  * @since commons-configuration 1.0
141  *
142  * @author J&ouml;rg Schaible
143  * @author Oliver Heger
144  * @version $Revision: 589380 $, $Date: 2007-10-28 17:37:35 +0100 (So, 28 Okt 2007) $
145  */
146 public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
147     implements EntityResolver
148 {
149     /***
150      * The serial version UID.
151      */
152     private static final long serialVersionUID = 2453781111653383552L;
153 
154     /*** Constant for the default root element name. */
155     private static final String DEFAULT_ROOT_NAME = "configuration";
156 
157     /*** Constant for the delimiter for multiple attribute values.*/
158     private static final char ATTR_VALUE_DELIMITER = '|';
159 
160     /*** The document from this configuration's data source. */
161     private Document document;
162 
163     /*** Stores a map with the registered public IDs.*/
164     private Map registeredEntities = new HashMap();
165 
166     /*** Stores the name of the root element. */
167     private String rootElementName;
168 
169     /*** Stores the public ID from the DOCTYPE.*/
170     private String publicID;
171 
172     /*** Stores the system ID from the DOCTYPE.*/
173     private String systemID;
174 
175     /*** Stores the document builder that should be used for loading.*/
176     private DocumentBuilder documentBuilder;
177 
178     /*** Stores a flag whether DTD validation should be performed.*/
179     private boolean validating;
180 
181     /***
182      * Creates a new instance of <code>XMLConfiguration</code>.
183      */
184     public XMLConfiguration()
185     {
186         super();
187     }
188 
189     /***
190      * Creates a new instance of <code>XMLConfiguration</code> and copies the
191      * content of the passed in configuration into this object. Note that only
192      * the data of the passed in configuration will be copied. If, for instance,
193      * the other configuration is a <code>XMLConfiguration</code>, too,
194      * things like comments or processing instructions will be lost.
195      *
196      * @param c the configuration to copy
197      * @since 1.4
198      */
199     public XMLConfiguration(HierarchicalConfiguration c)
200     {
201         super(c);
202         clearReferences(getRootNode());
203     }
204 
205     /***
206      * Creates a new instance of <code>XMLConfiguration</code>. The
207      * configuration is loaded from the specified file
208      *
209      * @param fileName the name of the file to load
210      * @throws ConfigurationException if the file cannot be loaded
211      */
212     public XMLConfiguration(String fileName) throws ConfigurationException
213     {
214         super(fileName);
215     }
216 
217     /***
218      * Creates a new instance of <code>XMLConfiguration</code>.
219      * The configuration is loaded from the specified file.
220      *
221      * @param file the file
222      * @throws ConfigurationException if an error occurs while loading the file
223      */
224     public XMLConfiguration(File file) throws ConfigurationException
225     {
226         super(file);
227     }
228 
229     /***
230      * Creates a new instance of <code>XMLConfiguration</code>.
231      * The configuration is loaded from the specified URL.
232      *
233      * @param url the URL
234      * @throws ConfigurationException if loading causes an error
235      */
236     public XMLConfiguration(URL url) throws ConfigurationException
237     {
238         super(url);
239     }
240 
241     /***
242      * Returns the name of the root element. If this configuration was loaded
243      * from a XML document, the name of this document's root element is
244      * returned. Otherwise it is possible to set a name for the root element
245      * that will be used when this configuration is stored.
246      *
247      * @return the name of the root element
248      */
249     public String getRootElementName()
250     {
251         if (getDocument() == null)
252         {
253             return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
254         }
255         else
256         {
257             return getDocument().getDocumentElement().getNodeName();
258         }
259     }
260 
261     /***
262      * Sets the name of the root element. This name is used when this
263      * configuration object is stored in an XML file. Note that setting the name
264      * of the root element works only if this configuration has been newly
265      * created. If the configuration was loaded from an XML file, the name
266      * cannot be changed and an <code>UnsupportedOperationException</code>
267      * exception is thrown. Whether this configuration has been loaded from an
268      * XML document or not can be found out using the <code>getDocument()</code>
269      * method.
270      *
271      * @param name the name of the root element
272      */
273     public void setRootElementName(String name)
274     {
275         if (getDocument() != null)
276         {
277             throw new UnsupportedOperationException("The name of the root element "
278                     + "cannot be changed when loaded from an XML document!");
279         }
280         rootElementName = name;
281     }
282 
283     /***
284      * Returns the <code>DocumentBuilder</code> object that is used for
285      * loading documents. If no specific builder has been set, this method
286      * returns <b>null</b>.
287      *
288      * @return the <code>DocumentBuilder</code> for loading new documents
289      * @since 1.2
290      */
291     public DocumentBuilder getDocumentBuilder()
292     {
293         return documentBuilder;
294     }
295 
296     /***
297      * Sets the <code>DocumentBuilder</code> object to be used for loading
298      * documents. This method makes it possible to specify the exact document
299      * builder. So an application can create a builder, configure it for its
300      * special needs, and then pass it to this method.
301      *
302      * @param documentBuilder the document builder to be used; if undefined, a
303      * default builder will be used
304      * @since 1.2
305      */
306     public void setDocumentBuilder(DocumentBuilder documentBuilder)
307     {
308         this.documentBuilder = documentBuilder;
309     }
310 
311     /***
312      * Returns the public ID of the DOCTYPE declaration from the loaded XML
313      * document. This is <b>null</b> if no document has been loaded yet or if
314      * the document does not contain a DOCTYPE declaration with a public ID.
315      *
316      * @return the public ID
317      * @since 1.3
318      */
319     public String getPublicID()
320     {
321         return publicID;
322     }
323 
324     /***
325      * Sets the public ID of the DOCTYPE declaration. When this configuration is
326      * saved, a DOCTYPE declaration will be constructed that contains this
327      * public ID.
328      *
329      * @param publicID the public ID
330      * @since 1.3
331      */
332     public void setPublicID(String publicID)
333     {
334         this.publicID = publicID;
335     }
336 
337     /***
338      * Returns the system ID of the DOCTYPE declaration from the loaded XML
339      * document. This is <b>null</b> if no document has been loaded yet or if
340      * the document does not contain a DOCTYPE declaration with a system ID.
341      *
342      * @return the system ID
343      * @since 1.3
344      */
345     public String getSystemID()
346     {
347         return systemID;
348     }
349 
350     /***
351      * Sets the system ID of the DOCTYPE declaration. When this configuration is
352      * saved, a DOCTYPE declaration will be constructed that contains this
353      * system ID.
354      *
355      * @param systemID the system ID
356      * @since 1.3
357      */
358     public void setSystemID(String systemID)
359     {
360         this.systemID = systemID;
361     }
362 
363     /***
364      * Returns the value of the validating flag.
365      *
366      * @return the validating flag
367      * @since 1.2
368      */
369     public boolean isValidating()
370     {
371         return validating;
372     }
373 
374     /***
375      * Sets the value of the validating flag. This flag determines whether
376      * DTD validation should be performed when loading XML documents. This
377      * flag is evaluated only if no custom <code>DocumentBuilder</code> was set.
378      *
379      * @param validating the validating flag
380      * @since 1.2
381      */
382     public void setValidating(boolean validating)
383     {
384         this.validating = validating;
385     }
386 
387     /***
388      * Returns the XML document this configuration was loaded from. The return
389      * value is <b>null</b> if this configuration was not loaded from a XML
390      * document.
391      *
392      * @return the XML document this configuration was loaded from
393      */
394     public Document getDocument()
395     {
396         return document;
397     }
398 
399     /***
400      * Removes all properties from this configuration. If this configuration
401      * was loaded from a file, the associated DOM document is also cleared.
402      */
403     public void clear()
404     {
405         super.clear();
406         document = null;
407     }
408 
409     /***
410      * Initializes this configuration from an XML document.
411      *
412      * @param document the document to be parsed
413      * @param elemRefs a flag whether references to the XML elements should be set
414      */
415     public void initProperties(Document document, boolean elemRefs)
416     {
417         if (document.getDoctype() != null)
418         {
419             setPublicID(document.getDoctype().getPublicId());
420             setSystemID(document.getDoctype().getSystemId());
421         }
422 
423         constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
424         if (elemRefs)
425         {
426             getRoot().setReference(document.getDocumentElement());
427         }
428     }
429 
430     /***
431      * Helper method for building the internal storage hierarchy. The XML
432      * elements are transformed into node objects.
433      *
434      * @param node the actual node
435      * @param element the actual XML element
436      * @param elemRefs a flag whether references to the XML elements should be set
437      */
438     private void constructHierarchy(Node node, Element element, boolean elemRefs)
439     {
440         processAttributes(node, element, elemRefs);
441         StringBuffer buffer = new StringBuffer();
442         NodeList list = element.getChildNodes();
443         for (int i = 0; i < list.getLength(); i++)
444         {
445             org.w3c.dom.Node w3cNode = list.item(i);
446             if (w3cNode instanceof Element)
447             {
448                 Element child = (Element) w3cNode;
449                 Node childNode = new XMLNode(child.getTagName(),
450                         elemRefs ? child : null);
451                 constructHierarchy(childNode, child, elemRefs);
452                 node.addChild(childNode);
453                 handleDelimiters(node, childNode);
454             }
455             else if (w3cNode instanceof Text)
456             {
457                 Text data = (Text) w3cNode;
458                 buffer.append(data.getData());
459             }
460         }
461         String text = buffer.toString().trim();
462         if (text.length() > 0 || !node.hasChildren())
463         {
464             node.setValue(text);
465         }
466     }
467 
468     /***
469      * Helper method for constructing node objects for the attributes of the
470      * given XML element.
471      *
472      * @param node the actual node
473      * @param element the actual XML element
474      * @param elemRefs a flag whether references to the XML elements should be set
475      */
476     private void processAttributes(Node node, Element element, boolean elemRefs)
477     {
478         NamedNodeMap attributes = element.getAttributes();
479         for (int i = 0; i < attributes.getLength(); ++i)
480         {
481             org.w3c.dom.Node w3cNode = attributes.item(i);
482             if (w3cNode instanceof Attr)
483             {
484                 Attr attr = (Attr) w3cNode;
485                 Iterator it = PropertyConverter.split(
486                         attr.getValue(),
487                         isDelimiterParsingDisabled() ? ATTR_VALUE_DELIMITER
488                                 : getListDelimiter()).iterator();
489                 while (it.hasNext())
490                 {
491                     Node child = new XMLNode(attr.getName(),
492                             elemRefs ? element : null);
493                     child.setValue(it.next());
494                     node.addAttribute(child);
495                 }
496             }
497         }
498     }
499 
500     /***
501      * Deals with elements whose value is a list. In this case multiple child
502      * elements must be added.
503      *
504      * @param parent the parent element
505      * @param child the child element
506      */
507     private void handleDelimiters(Node parent, Node child)
508     {
509         if (child.getValue() != null)
510         {
511             List values;
512             if (isDelimiterParsingDisabled())
513             {
514                 values = new ArrayList();
515                 values.add(child.getValue().toString());
516             }
517             else
518             {
519                 values = PropertyConverter.split(child.getValue().toString(),
520                     getListDelimiter());
521             }
522 
523             if (values.size() > 1)
524             {
525                 Iterator it = values.iterator();
526                 // Create new node for the original child's first value
527                 Node c = createNode(child.getName());
528                 c.setValue(it.next());
529                 // Copy original attributes to the new node
530                 for (Iterator itAttrs = child.getAttributes().iterator(); itAttrs
531                         .hasNext();)
532                 {
533                     Node ndAttr = (Node) itAttrs.next();
534                     ndAttr.setReference(null);
535                     c.addAttribute(ndAttr);
536                 }
537                 parent.remove(child);
538                 parent.addChild(c);
539 
540                 // add multiple new children
541                 while (it.hasNext())
542                 {
543                     c = new XMLNode(child.getName(), null);
544                     c.setValue(it.next());
545                     parent.addChild(c);
546                 }
547             }
548             else if (values.size() == 1)
549             {
550                 // we will have to replace the value because it might
551                 // contain escaped delimiters
552                 child.setValue(values.get(0));
553             }
554         }
555     }
556 
557     /***
558      * Creates the <code>DocumentBuilder</code> to be used for loading files.
559      * This implementation checks whether a specific
560      * <code>DocumentBuilder</code> has been set. If this is the case, this
561      * one is used. Otherwise a default builder is created. Depending on the
562      * value of the validating flag this builder will be a validating or a non
563      * validating <code>DocumentBuilder</code>.
564      *
565      * @return the <code>DocumentBuilder</code> for loading configuration
566      * files
567      * @throws ParserConfigurationException if an error occurs
568      * @since 1.2
569      */
570     protected DocumentBuilder createDocumentBuilder()
571             throws ParserConfigurationException
572     {
573         if (getDocumentBuilder() != null)
574         {
575             return getDocumentBuilder();
576         }
577         else
578         {
579             DocumentBuilderFactory factory = DocumentBuilderFactory
580                     .newInstance();
581             factory.setValidating(isValidating());
582             DocumentBuilder result = factory.newDocumentBuilder();
583             result.setEntityResolver(this);
584 
585             if (isValidating())
586             {
587                 // register an error handler which detects validation errors
588                 result.setErrorHandler(new DefaultHandler()
589                 {
590                     public void error(SAXParseException ex) throws SAXException
591                     {
592                         throw ex;
593                     }
594                 });
595             }
596             return result;
597         }
598     }
599 
600     /***
601      * Creates a DOM document from the internal tree of configuration nodes.
602      *
603      * @return the new document
604      * @throws ConfigurationException if an error occurs
605      */
606     protected Document createDocument() throws ConfigurationException
607     {
608         try
609         {
610             if (document == null)
611             {
612                 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
613                 Document newDocument = builder.newDocument();
614                 Element rootElem = newDocument.createElement(getRootElementName());
615                 newDocument.appendChild(rootElem);
616                 document = newDocument;
617             }
618 
619             XMLBuilderVisitor builder = new XMLBuilderVisitor(document,
620                     isDelimiterParsingDisabled() ? (char) 0 : getListDelimiter());
621             builder.processDocument(getRoot());
622             return document;
623         } /* try */
624         catch (DOMException domEx)
625         {
626             throw new ConfigurationException(domEx);
627         }
628         catch (ParserConfigurationException pex)
629         {
630             throw new ConfigurationException(pex);
631         }
632     }
633 
634     /***
635      * Creates a new node object. This implementation returns an instance of the
636      * <code>XMLNode</code> class.
637      *
638      * @param name the node's name
639      * @return the new node
640      */
641     protected Node createNode(String name)
642     {
643         return new XMLNode(name, null);
644     }
645 
646     /***
647      * Loads the configuration from the given input stream.
648      *
649      * @param in the input stream
650      * @throws ConfigurationException if an error occurs
651      */
652     public void load(InputStream in) throws ConfigurationException
653     {
654         load(new InputSource(in));
655     }
656 
657     /***
658      * Load the configuration from the given reader.
659      * Note that the <code>clear()</code> method is not called, so
660      * the properties contained in the loaded file will be added to the
661      * actual set of properties.
662      *
663      * @param in An InputStream.
664      *
665      * @throws ConfigurationException if an error occurs
666      */
667     public void load(Reader in) throws ConfigurationException
668     {
669         load(new InputSource(in));
670     }
671 
672     /***
673      * Loads a configuration file from the specified input source.
674      * @param source the input source
675      * @throws ConfigurationException if an error occurs
676      */
677     private void load(InputSource source) throws ConfigurationException
678     {
679         try
680         {
681             URL sourceURL = getDelegate().getURL();
682             if (sourceURL != null)
683             {
684                 source.setSystemId(sourceURL.toString());
685             }
686 
687             DocumentBuilder builder = createDocumentBuilder();
688             Document newDocument = builder.parse(source);
689             Document oldDocument = document;
690             document = null;
691             initProperties(newDocument, oldDocument == null);
692             document = (oldDocument == null) ? newDocument : oldDocument;
693         }
694         catch (Exception e)
695         {
696             throw new ConfigurationException(e.getMessage(), e);
697         }
698     }
699 
700     /***
701      * Saves the configuration to the specified writer.
702      *
703      * @param writer the writer used to save the configuration
704      * @throws ConfigurationException if an error occurs
705      */
706     public void save(Writer writer) throws ConfigurationException
707     {
708         try
709         {
710             Transformer transformer = createTransformer();
711             Source source = new DOMSource(createDocument());
712             Result result = new StreamResult(writer);
713             transformer.transform(source, result);
714         }
715         catch (TransformerException e)
716         {
717             throw new ConfigurationException(e.getMessage(), e);
718         }
719         catch (TransformerFactoryConfigurationError err)
720         {
721             throw new ConfigurationException(err.getMessage(), err);
722         }
723     }
724 
725     /***
726      * Creates and initializes the transformer used for save operations. This
727      * base implementation initializes all of the default settings like
728      * indention mode and the DOCTYPE. Derived classes may overload this method
729      * if they have specific needs.
730      *
731      * @return the transformer to use for a save operation
732      * @throws TransformerException if an error occurs
733      * @since 1.3
734      */
735     protected Transformer createTransformer() throws TransformerException
736     {
737         Transformer transformer = TransformerFactory.newInstance()
738                 .newTransformer();
739 
740         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
741         if (getEncoding() != null)
742         {
743             transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
744         }
745         if (getPublicID() != null)
746         {
747             transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
748                     getPublicID());
749         }
750         if (getSystemID() != null)
751         {
752             transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
753                     getSystemID());
754         }
755 
756         return transformer;
757     }
758 
759     /***
760      * Creates a copy of this object. The new configuration object will contain
761      * the same properties as the original, but it will lose any connection to a
762      * source document (if one exists). This is to avoid race conditions if both
763      * the original and the copy are modified and then saved.
764      *
765      * @return the copy
766      */
767     public Object clone()
768     {
769         XMLConfiguration copy = (XMLConfiguration) super.clone();
770 
771         // clear document related properties
772         copy.document = null;
773         copy.setDelegate(copy.createDelegate());
774         // clear all references in the nodes, too
775         clearReferences(copy.getRootNode());
776 
777         return copy;
778     }
779 
780     /***
781      * Creates the file configuration delegate for this object. This implementation
782      * will return an instance of a class derived from <code>FileConfigurationDelegate</code>
783      * that deals with some specialities of <code>XMLConfiguration</code>.
784      * @return the delegate for this object
785      */
786     protected FileConfigurationDelegate createDelegate()
787     {
788         return new XMLFileConfigurationDelegate();
789     }
790 
791     /***
792      * Adds a collection of nodes directly to this configuration. This
793      * implementation ensures that the nodes to be added are of the correct node
794      * type (they have to be converted to <code>XMLNode</code> if necessary).
795      *
796      * @param key the key where the nodes are to be added
797      * @param nodes the collection with the new nodes
798      * @since 1.5
799      */
800     public void addNodes(String key, Collection nodes)
801     {
802         Collection xmlNodes;
803 
804         if (nodes != null && !nodes.isEmpty())
805         {
806             xmlNodes = new ArrayList(nodes.size());
807             for (Iterator it = nodes.iterator(); it.hasNext();)
808             {
809                 xmlNodes.add(convertToXMLNode((ConfigurationNode) it.next()));
810             }
811         }
812         else
813         {
814             xmlNodes = nodes;
815         }
816 
817         super.addNodes(key, xmlNodes);
818     }
819 
820     /***
821      * Converts the specified node into a <code>XMLNode</code> if necessary.
822      * This is required for nodes that are directly added, e.g. by
823      * <code>addNodes()</code>. If the passed in node is already an instance
824      * of <code>XMLNode</code>, it is directly returned, and conversion
825      * stops. Otherwise a new <code>XMLNode</code> is created, and the
826      * children are also converted.
827      *
828      * @param node the node to be converted
829      * @return the converted node
830      */
831     private XMLNode convertToXMLNode(ConfigurationNode node)
832     {
833         if (node instanceof XMLNode)
834         {
835             return (XMLNode) node;
836         }
837 
838         XMLNode nd = (XMLNode) createNode(node.getName());
839         nd.setValue(node.getValue());
840         for (Iterator it = node.getChildren().iterator(); it.hasNext();)
841         {
842             nd.addChild(convertToXMLNode((ConfigurationNode) it.next()));
843         }
844         for (Iterator it = node.getAttributes().iterator(); it.hasNext();)
845         {
846             nd.addAttribute(convertToXMLNode((ConfigurationNode) it.next()));
847         }
848         return nd;
849     }
850 
851     /***
852      * <p>
853      * Registers the specified DTD URL for the specified public identifier.
854      * </p>
855      * <p>
856      * <code>XMLConfiguration</code> contains an internal
857      * <code>EntityResolver</code> implementation. This maps
858      * <code>PUBLICID</code>'s to URLs (from which the resource will be
859      * loaded). A common use case for this method is to register local URLs
860      * (possibly computed at runtime by a class loader) for DTDs. This allows
861      * the performance advantage of using a local version without having to
862      * ensure every <code>SYSTEM</code> URI on every processed XML document is
863      * local. This implementation provides only basic functionality. If more
864      * sophisticated features are required, using
865      * {@link #setDocumentBuilder(DocumentBuilder)} to set a custom
866      * <code>DocumentBuilder</code> (which also can be initialized with a
867      * custom <code>EntityResolver</code>) is recommended.
868      * </p>
869      * <p>
870      * <strong>Note:</strong> This method will have no effect when a custom
871      * <code>DocumentBuilder</code> has been set. (Setting a custom
872      * <code>DocumentBuilder</code> overrides the internal implementation.)
873      * </p>
874      * <p>
875      * <strong>Note:</strong> This method must be called before the
876      * configuration is loaded. So the default constructor of
877      * <code>XMLConfiguration</code> should be used, the location of the
878      * configuration file set, <code>registerEntityId()</code> called, and
879      * finally the <code>load()</code> method can be invoked.
880      * </p>
881      *
882      * @param publicId Public identifier of the DTD to be resolved
883      * @param entityURL The URL to use for reading this DTD
884      * @throws IllegalArgumentException if the public ID is undefined
885      * @since 1.5
886      */
887     public void registerEntityId(String publicId, URL entityURL)
888     {
889         if (publicId == null)
890         {
891             throw new IllegalArgumentException("Public ID must not be null!");
892         }
893         getRegisteredEntities().put(publicId, entityURL);
894     }
895 
896     /***
897      * Resolves the requested external entity. This is the default
898      * implementation of the <code>EntityResolver</code> interface. It checks
899      * the passed in public ID against the registered entity IDs and uses a
900      * local URL if possible.
901      *
902      * @param publicId the public identifier of the entity being referenced
903      * @param systemId the system identifier of the entity being referenced
904      * @return an input source for the specified entity
905      * @throws SAXException if a parsing exception occurs
906      * @since 1.5
907      */
908     public InputSource resolveEntity(String publicId, String systemId)
909             throws SAXException
910     {
911         // Has this system identifier been registered?
912         URL entityURL = null;
913         if (publicId != null)
914         {
915             entityURL = (URL) getRegisteredEntities().get(publicId);
916         }
917 
918         if (entityURL != null)
919         {
920             // Obtain an InputSource for this URL. This code is based on the
921             // createInputSourceFromURL() method of Commons Digester.
922             try
923             {
924                 URLConnection connection = entityURL.openConnection();
925                 connection.setUseCaches(false);
926                 InputStream stream = connection.getInputStream();
927                 InputSource source = new InputSource(stream);
928                 source.setSystemId(entityURL.toExternalForm());
929                 return source;
930             }
931             catch (IOException e)
932             {
933                 throw new SAXException(e);
934             }
935         }
936         else
937         {
938             // default processing behavior
939             return null;
940         }
941     }
942 
943     /***
944      * Returns a map with the entity IDs that have been registered using the
945      * <code>registerEntityId()</code> method.
946      *
947      * @return a map with the registered entity IDs
948      */
949     Map getRegisteredEntities()
950     {
951         return registeredEntities;
952     }
953 
954     /***
955      * A specialized <code>Node</code> class that is connected with an XML
956      * element. Changes on a node are also performed on the associated element.
957      */
958     class XMLNode extends Node
959     {
960         /***
961          * The serial version UID.
962          */
963         private static final long serialVersionUID = -4133988932174596562L;
964 
965         /***
966          * Creates a new instance of <code>XMLNode</code> and initializes it
967          * with a name and the corresponding XML element.
968          *
969          * @param name the node's name
970          * @param elem the XML element
971          */
972         public XMLNode(String name, Element elem)
973         {
974             super(name);
975             setReference(elem);
976         }
977 
978         /***
979          * Sets the value of this node. If this node is associated with an XML
980          * element, this element will be updated, too.
981          *
982          * @param value the node's new value
983          */
984         public void setValue(Object value)
985         {
986             super.setValue(value);
987 
988             if (getReference() != null && document != null)
989             {
990                 if (isAttribute())
991                 {
992                     updateAttribute();
993                 }
994                 else
995                 {
996                     updateElement(value);
997                 }
998             }
999         }
1000 
1001         /***
1002          * Updates the associated XML elements when a node is removed.
1003          */
1004         protected void removeReference()
1005         {
1006             if (getReference() != null)
1007             {
1008                 Element element = (Element) getReference();
1009                 if (isAttribute())
1010                 {
1011                     updateAttribute();
1012                 }
1013                 else
1014                 {
1015                     org.w3c.dom.Node parentElem = element.getParentNode();
1016                     if (parentElem != null)
1017                     {
1018                         parentElem.removeChild(element);
1019                     }
1020                 }
1021             }
1022         }
1023 
1024         /***
1025          * Updates the node's value if it represents an element node.
1026          *
1027          * @param value the new value
1028          */
1029         private void updateElement(Object value)
1030         {
1031             Text txtNode = findTextNodeForUpdate();
1032             if (value == null)
1033             {
1034                 // remove text
1035                 if (txtNode != null)
1036                 {
1037                     ((Element) getReference()).removeChild(txtNode);
1038                 }
1039             }
1040             else
1041             {
1042                 if (txtNode == null)
1043                 {
1044                     txtNode = document
1045                             .createTextNode(PropertyConverter.escapeDelimiters(
1046                                     value.toString(), getListDelimiter()));
1047                     if (((Element) getReference()).getFirstChild() != null)
1048                     {
1049                         ((Element) getReference()).insertBefore(txtNode,
1050                                 ((Element) getReference()).getFirstChild());
1051                     }
1052                     else
1053                     {
1054                         ((Element) getReference()).appendChild(txtNode);
1055                     }
1056                 }
1057                 else
1058                 {
1059                     txtNode.setNodeValue(PropertyConverter.escapeDelimiters(
1060                             value.toString(), getListDelimiter()));
1061                 }
1062             }
1063         }
1064 
1065         /***
1066          * Updates the node's value if it represents an attribute.
1067          *
1068          */
1069         private void updateAttribute()
1070         {
1071             XMLBuilderVisitor.updateAttribute(getParent(), getName(), getListDelimiter());
1072         }
1073 
1074         /***
1075          * Returns the only text node of this element for update. This method is
1076          * called when the element's text changes. Then all text nodes except
1077          * for the first are removed. A reference to the first is returned or
1078          * <b>null </b> if there is no text node at all.
1079          *
1080          * @return the first and only text node
1081          */
1082         private Text findTextNodeForUpdate()
1083         {
1084             Text result = null;
1085             Element elem = (Element) getReference();
1086             // Find all Text nodes
1087             NodeList children = elem.getChildNodes();
1088             Collection textNodes = new ArrayList();
1089             for (int i = 0; i < children.getLength(); i++)
1090             {
1091                 org.w3c.dom.Node nd = children.item(i);
1092                 if (nd instanceof Text)
1093                 {
1094                     if (result == null)
1095                     {
1096                         result = (Text) nd;
1097                     }
1098                     else
1099                     {
1100                         textNodes.add(nd);
1101                     }
1102                 }
1103             }
1104 
1105             // We don't want CDATAs
1106             if (result instanceof CDATASection)
1107             {
1108                 textNodes.add(result);
1109                 result = null;
1110             }
1111 
1112             // Remove all but the first Text node
1113             for (Iterator it = textNodes.iterator(); it.hasNext();)
1114             {
1115                 elem.removeChild((org.w3c.dom.Node) it.next());
1116             }
1117             return result;
1118         }
1119     }
1120 
1121     /***
1122      * A concrete <code>BuilderVisitor</code> that can construct XML
1123      * documents.
1124      */
1125     static class XMLBuilderVisitor extends BuilderVisitor
1126     {
1127         /*** Stores the document to be constructed. */
1128         private Document document;
1129 
1130         /*** Stores the list delimiter.*/
1131         private char listDelimiter = AbstractConfiguration.
1132                 getDefaultListDelimiter();
1133 
1134         /***
1135          * Creates a new instance of <code>XMLBuilderVisitor</code>
1136          *
1137          * @param doc the document to be created
1138          * @param listDelimiter the delimiter for attribute properties with multiple values
1139          */
1140         public XMLBuilderVisitor(Document doc, char listDelimiter)
1141         {
1142             document = doc;
1143             this.listDelimiter = listDelimiter;
1144         }
1145 
1146         /***
1147          * Processes the node hierarchy and adds new nodes to the document.
1148          *
1149          * @param rootNode the root node
1150          */
1151         public void processDocument(Node rootNode)
1152         {
1153             rootNode.visit(this, null);
1154         }
1155 
1156         /***
1157          * Inserts a new node. This implementation ensures that the correct
1158          * XML element is created and inserted between the given siblings.
1159          *
1160          * @param newNode the node to insert
1161          * @param parent the parent node
1162          * @param sibling1 the first sibling
1163          * @param sibling2 the second sibling
1164          * @return the new node
1165          */
1166         protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
1167         {
1168             if (newNode.isAttribute())
1169             {
1170                 updateAttribute(parent, getElement(parent), newNode.getName(), listDelimiter);
1171                 return null;
1172             }
1173 
1174             else
1175             {
1176                 Element elem = document.createElement(newNode.getName());
1177                 if (newNode.getValue() != null)
1178                 {
1179                     String txt = newNode.getValue().toString();
1180                     if (listDelimiter != 0)
1181                     {
1182                         txt = PropertyConverter.escapeDelimiters(txt, listDelimiter);
1183                     }
1184                     elem.appendChild(document.createTextNode(txt));
1185                 }
1186                 if (sibling2 == null)
1187                 {
1188                     getElement(parent).appendChild(elem);
1189                 }
1190                 else if (sibling1 != null)
1191                 {
1192                     getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
1193                 }
1194                 else
1195                 {
1196                     getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
1197                 }
1198                 return elem;
1199             }
1200         }
1201 
1202         /***
1203          * Helper method for updating the value of the specified node's
1204          * attribute with the given name.
1205          *
1206          * @param node the affected node
1207          * @param elem the element that is associated with this node
1208          * @param name the name of the affected attribute
1209          * @param listDelimiter the delimiter for attributes with multiple values
1210          */
1211         private static void updateAttribute(Node node, Element elem, String name, char listDelimiter)
1212         {
1213             if (node != null && elem != null)
1214             {
1215                 List attrs = node.getAttributes(name);
1216                 StringBuffer buf = new StringBuffer();
1217                 char delimiter = (listDelimiter != 0) ? listDelimiter : ATTR_VALUE_DELIMITER;
1218                 for (Iterator it = attrs.iterator(); it.hasNext();)
1219                 {
1220                     Node attr = (Node) it.next();
1221                     if (attr.getValue() != null)
1222                     {
1223                         if (buf.length() > 0)
1224                         {
1225                             buf.append(delimiter);
1226                         }
1227                         buf.append(PropertyConverter.escapeDelimiters(attr
1228                                 .getValue().toString(), delimiter));
1229                     }
1230                     attr.setReference(elem);
1231                 }
1232 
1233                 if (buf.length() < 1)
1234                 {
1235                     elem.removeAttribute(name);
1236                 }
1237                 else
1238                 {
1239                     elem.setAttribute(name, buf.toString());
1240                 }
1241             }
1242         }
1243 
1244         /***
1245          * Updates the value of the specified attribute of the given node.
1246          * Because there can be multiple child nodes representing this attribute
1247          * the new value is determined by iterating over all those child nodes.
1248          *
1249          * @param node the affected node
1250          * @param name the name of the attribute
1251          * @param listDelimiter the delimiter for attributes with multiple values
1252          */
1253         static void updateAttribute(Node node, String name, char listDelimiter)
1254         {
1255             if (node != null)
1256             {
1257                 updateAttribute(node, (Element) node.getReference(), name, listDelimiter);
1258             }
1259         }
1260 
1261         /***
1262          * Helper method for accessing the element of the specified node.
1263          *
1264          * @param node the node
1265          * @return the element of this node
1266          */
1267         private Element getElement(Node node)
1268         {
1269             // special treatement for root node of the hierarchy
1270             return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
1271         }
1272     }
1273 
1274     /***
1275      * A special implementation of the <code>FileConfiguration</code> interface that is
1276      * used internally to implement the <code>FileConfiguration</code> methods
1277      * for <code>XMLConfiguration</code>, too.
1278      */
1279     private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
1280     {
1281         public void load(InputStream in) throws ConfigurationException
1282         {
1283             XMLConfiguration.this.load(in);
1284         }
1285     }
1286 }