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