    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    }