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