    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
    018package org.apache.commons.configuration2;
    019
    020import javax.xml.parsers.DocumentBuilder;
    021import javax.xml.parsers.DocumentBuilderFactory;
    022import javax.xml.parsers.ParserConfigurationException;
    023import javax.xml.transform.OutputKeys;
    024import javax.xml.transform.Result;
    025import javax.xml.transform.Source;
    026import javax.xml.transform.Transformer;
    027import javax.xml.transform.dom.DOMSource;
    028import javax.xml.transform.stream.StreamResult;
    029import java.io.IOException;
    030import java.io.InputStream;
    031import java.io.Reader;
    032import java.io.StringReader;
    033import java.io.StringWriter;
    034import java.io.Writer;
    035import java.net.URL;
    036import java.util.ArrayList;
    037import java.util.Collection;
    038import java.util.Collections;
    039import java.util.HashMap;
    040import java.util.Iterator;
    041import java.util.Map;
    042
    043import org.apache.commons.configuration2.convert.ListDelimiterHandler;
    044import org.apache.commons.configuration2.ex.ConfigurationException;
    045import org.apache.commons.configuration2.io.ConfigurationLogger;
    046import org.apache.commons.configuration2.io.FileLocator;
    047import org.apache.commons.configuration2.io.FileLocatorAware;
    048import org.apache.commons.configuration2.io.InputStreamSupport;
    049import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
    050import org.apache.commons.configuration2.tree.ImmutableNode;
    051import org.apache.commons.configuration2.tree.NodeTreeWalker;
    052import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
    053import org.apache.commons.lang3.StringUtils;
    054import org.apache.commons.lang3.mutable.MutableObject;
    055import org.w3c.dom.Attr;
    056import org.w3c.dom.CDATASection;
    057import org.w3c.dom.Document;
    058import org.w3c.dom.Element;
    059import org.w3c.dom.NamedNodeMap;
    060import org.w3c.dom.Node;
    061import org.w3c.dom.NodeList;
    062import org.w3c.dom.Text;
    063import org.xml.sax.EntityResolver;
    064import org.xml.sax.InputSource;
    065import org.xml.sax.SAXException;
    066import org.xml.sax.SAXParseException;
    067import org.xml.sax.helpers.DefaultHandler;
    068
    069/**
    070 * <p>
    071 * A specialized hierarchical configuration class that is able to parse XML
    072 * documents.
    073 * </p>
    074 * <p>
    075 * The parsed document will be stored keeping its structure. The class also
    076 * tries to preserve as much information from the loaded XML document as
    077 * possible, including comments and processing instructions. These will be
    078 * contained in documents created by the {@code save()} methods, too.
    079 * </p>
    080 * <p>
    081 * Like other file based configuration classes this class maintains the name and
    082 * path to the loaded configuration file. These properties can be altered using
    083 * several setter methods, but they are not modified by {@code save()} and
    084 * {@code load()} methods. If XML documents contain relative paths to other
    085 * documents (e.g. to a DTD), these references are resolved based on the path
    086 * set for this configuration.
    087 * </p>
    088 * <p>
    089 * By inheriting from {@link AbstractConfiguration} this class provides some
    090 * extended functionality, e.g. interpolation of property values. Like in
    091 * {@link PropertiesConfiguration} property values can contain delimiter
    092 * characters (the comma ',' per default) and are then split into multiple
    093 * values. This works for XML attributes and text content of elements as well.
    094 * The delimiter can be escaped by a backslash. As an example consider the
    095 * following XML fragment:
    096 * </p>
    097 *
    098 * <pre>
    099 * &lt;config&gt;
    100 *   &lt;array&gt;10,20,30,40&lt;/array&gt;
    101 *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
    102 *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
    103 * &lt;/config&gt;
    104 * </pre>
    105 *
    106 * <p>
    107 * Here the content of the {@code array} element will be split at the commas, so
    108 * the {@code array} key will be assigned 4 values. In the {@code scalar}
    109 * property and the {@code text} attribute of the {@code cite} element the comma
    110 * is escaped, so that no splitting is performed.
    111 * </p>
    112 * <p>
    113 * The configuration API allows setting multiple values for a single attribute,
    114 * e.g. something like the following is legal (assuming that the default
    115 * expression engine is used):
    116 * </p>
    117 *
    118 * <pre>
    119 * XMLConfiguration config = new XMLConfiguration();
    120 * config.addProperty(&quot;test.dir[@name]&quot;, &quot;C:\\Temp\\&quot;);
    121 * config.addProperty(&quot;test.dir[@name]&quot;, &quot;D:\\Data\\&quot;);
    122 * </pre>
    123 *
    124 * <p>
    125 * However, in XML such a constellation is not supported; an attribute can
    126 * appear only once for a single element. Therefore, an attempt to save a
    127 * configuration which violates this condition will throw an exception.
    128 * </p>
    129 * <p>
    130 * Like other {@code Configuration} implementations, {@code XMLConfiguration}
    131 * uses a {@link ListDelimiterHandler} object for controlling list split
    132 * operations. Per default, a list delimiter handler object is set which
    133 * disables this feature. XML has a built-in support for complex structures
    134 * including list properties; therefore, list splitting is not that relevant for
    135 * this configuration type. Nevertheless, by setting an alternative
    136 * {@code ListDelimiterHandler} implementation, this feature can be enabled. It
    137 * works as for any other concrete {@code Configuration} implementation.
    138 * </p>
    139 * <p>
    140 * Whitespace in the content of XML documents is trimmed per default. In most
    141 * cases this is desired. However, sometimes whitespace is indeed important and
    142 * should be treated as part of the value of a property as in the following
    143 * example:
    144 * </p>
    145 * <pre>
    146 *   &lt;indent&gt;    &lt;/indent&gt;
    147 * </pre>
    148 *
    149 * <p>
    150 * Per default the spaces in the {@code indent} element will be trimmed
    151 * resulting in an empty element. To tell {@code XMLConfiguration} that spaces
    152 * are relevant the {@code xml:space} attribute can be used, which is defined in
    153 * the <a href="http://www.w3.org/TR/REC-xml/#sec-white-space">XML
    154 * specification</a>. This will look as follows:
    155 * </p>
    156 * <pre>
    157 *   &lt;indent <strong>xml:space=&quot;preserve&quot;</strong>&gt;    &lt;/indent&gt;
    158 * </pre>
    159 *
    160 * <p>
    161 * The value of the {@code indent} property will now contain the spaces.
    162 * </p>
    163 * <p>
    164 * {@code XMLConfiguration} implements the {@link FileBasedConfiguration}
    165 * interface and thus can be used together with a file-based builder to load XML
    166 * configuration files from various sources like files, URLs, or streams.
    167 * </p>
    168 * <p>
    169 * Like other {@code Configuration} implementations, this class uses a
    170 * {@code Synchronizer} object to control concurrent access. By choosing a
    171 * suitable implementation of the {@code Synchronizer} interface, an instance
    172 * can be made thread-safe or not. Note that access to most of the properties
    173 * typically set through a builder is not protected by the {@code Synchronizer}.
    174 * The intended usage is that these properties are set once at construction time
    175 * through the builder and after that remain constant. If you wish to change
    176 * such properties during life time of an instance, you have to use the
    177 * {@code lock()} and {@code unlock()} methods manually to ensure that other
    178 * threads see your changes.
    179 * </p>
    180 * <p>
    181 * More information about the basic functionality supported by
    182 * {@code XMLConfiguration} can be found at the user's guide at
    183 * <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html">
    184 * Basic features and AbstractConfiguration</a>. There is
    185 * also a separate chapter dealing with
    186 * <a href="commons.apache.org/proper/commons-configuration/userguide/howto_xml.html">
    187 * XML Configurations</a> in special.
    188 * </p>
    189 *
    190 * @since commons-configuration 1.0
    191 */
    192public class XMLConfiguration extends BaseHierarchicalConfiguration implements
    193        FileBasedConfiguration, FileLocatorAware, InputStreamSupport
    194{
    195    /** Constant for the default indent size. */
    196    static final int DEFAULT_INDENT_SIZE = 2;
    197
    198    /** Constant for output property name used on a transformer to specify the indent amount. */
    199    static final String INDENT_AMOUNT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount";
    200
    201    /** Constant for the default root element name. */
    202    private static final String DEFAULT_ROOT_NAME = "configuration";
    203
    204    /** Constant for the name of the space attribute.*/
    205    private static final String ATTR_SPACE = "xml:space";
    206
    207    /** Constant for an internally used space attribute. */
    208    private static final String ATTR_SPACE_INTERNAL = "config-xml:space";
    209
    210    /** Constant for the xml:space value for preserving whitespace.*/
    211    private static final String VALUE_PRESERVE = "preserve";
    212
    213    /** Schema Langauge key for the parser */
    214    private static final String JAXP_SCHEMA_LANGUAGE =
    215        "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    216
    217    /** Schema Language for the parser */
    218    private static final String W3C_XML_SCHEMA =
    219        "http://www.w3.org/2001/XMLSchema";
    220
    221    /** Stores the name of the root element. */
    222    private String rootElementName;
    223
    224    /** Stores the public ID from the DOCTYPE.*/
    225    private String publicID;
    226
    227    /** Stores the system ID from the DOCTYPE.*/
    228    private String systemID;
    229
    230    /** Stores the document builder that should be used for loading.*/
    231    private DocumentBuilder documentBuilder;
    232
    233    /** Stores a flag whether DTD or Schema validation should be performed.*/
    234    private boolean validating;
    235
    236    /** Stores a flag whether DTD or Schema validation is used */
    237    private boolean schemaValidation;
    238
    239    /** The EntityResolver to use */
    240    private EntityResolver entityResolver = new DefaultEntityResolver();
    241
    242    /** The current file locator. */
    243    private FileLocator locator;
    244
    245    /**
    246     * Creates a new instance of {@code XMLConfiguration}.
    247     */
    248    public XMLConfiguration()
    249    {
    250        super();
    251        initLogger(new ConfigurationLogger(XMLConfiguration.class));
    252    }
    253
    254    /**
    255     * Creates a new instance of {@code XMLConfiguration} and copies the
    256     * content of the passed in configuration into this object. Note that only
    257     * the data of the passed in configuration will be copied. If, for instance,
    258     * the other configuration is a {@code XMLConfiguration}, too,
    259     * things like comments or processing instructions will be lost.
    260     *
    261     * @param c the configuration to copy
    262     * @since 1.4
    263     */
    264    public XMLConfiguration(final HierarchicalConfiguration<ImmutableNode> c)
    265    {
    266        super(c);
    267        rootElementName =
    268                c != null ? c.getRootElementName() : null;
    269        initLogger(new ConfigurationLogger(XMLConfiguration.class));
    270    }
    271
    272    /**
    273     * Returns the name of the root element. If this configuration was loaded
    274     * from a XML document, the name of this document's root element is
    275     * returned. Otherwise it is possible to set a name for the root element
    276     * that will be used when this configuration is stored.
    277     *
    278     * @return the name of the root element
    279     */
    280    @Override
    281    protected String getRootElementNameInternal()
    282    {
    283        final Document doc = getDocument();
    284        if (doc == null)
    285        {
    286            return rootElementName == null ? DEFAULT_ROOT_NAME : rootElementName;
    287        }
    288        return doc.getDocumentElement().getNodeName();
    289    }
    290
    291    /**
    292     * Sets the name of the root element. This name is used when this
    293     * configuration object is stored in an XML file. Note that setting the name
    294     * of the root element works only if this configuration has been newly
    295     * created. If the configuration was loaded from an XML file, the name
    296     * cannot be changed and an {@code UnsupportedOperationException}
    297     * exception is thrown. Whether this configuration has been loaded from an
    298     * XML document or not can be found out using the {@code getDocument()}
    299     * method.
    300     *
    301     * @param name the name of the root element
    302     */
    303    public void setRootElementName(final String name)
    304    {
    305        beginRead(true);
    306        try
    307        {
    308            if (getDocument() != null)
    309            {
    310                throw new UnsupportedOperationException(
    311                        "The name of the root element "
    312                                + "cannot be changed when loaded from an XML document!");
    313            }
    314            rootElementName = name;
    315        }
    316        finally
    317        {
    318            endRead();
    319        }
    320    }
    321
    322    /**
    323     * Returns the {@code DocumentBuilder} object that is used for
    324     * loading documents. If no specific builder has been set, this method
    325     * returns <b>null</b>.
    326     *
    327     * @return the {@code DocumentBuilder} for loading new documents
    328     * @since 1.2
    329     */
    330    public DocumentBuilder getDocumentBuilder()
    331    {
    332        return documentBuilder;
    333    }
    334
    335    /**
    336     * Sets the {@code DocumentBuilder} object to be used for loading
    337     * documents. This method makes it possible to specify the exact document
    338     * builder. So an application can create a builder, configure it for its
    339     * special needs, and then pass it to this method.
    340     *
    341     * @param documentBuilder the document builder to be used; if undefined, a
    342     * default builder will be used
    343     * @since 1.2
    344     */
    345    public void setDocumentBuilder(final DocumentBuilder documentBuilder)
    346    {
    347        this.documentBuilder = documentBuilder;
    348    }
    349
    350    /**
    351     * Returns the public ID of the DOCTYPE declaration from the loaded XML
    352     * document. This is <b>null</b> if no document has been loaded yet or if
    353     * the document does not contain a DOCTYPE declaration with a public ID.
    354     *
    355     * @return the public ID
    356     * @since 1.3
    357     */
    358    public String getPublicID()
    359    {
    360        beginRead(false);
    361        try
    362        {
    363            return publicID;
    364        }
    365        finally
    366        {
    367            endRead();
    368        }
    369    }
    370
    371    /**
    372     * Sets the public ID of the DOCTYPE declaration. When this configuration is
    373     * saved, a DOCTYPE declaration will be constructed that contains this
    374     * public ID.
    375     *
    376     * @param publicID the public ID
    377     * @since 1.3
    378     */
    379    public void setPublicID(final String publicID)
    380    {
    381        beginWrite(false);
    382        try
    383        {
    384            this.publicID = publicID;
    385        }
    386        finally
    387        {
    388            endWrite();
    389        }
    390    }
    391
    392    /**
    393     * Returns the system ID of the DOCTYPE declaration from the loaded XML
    394     * document. This is <b>null</b> if no document has been loaded yet or if
    395     * the document does not contain a DOCTYPE declaration with a system ID.
    396     *
    397     * @return the system ID
    398     * @since 1.3
    399     */
    400    public String getSystemID()
    401    {
    402        beginRead(false);
    403        try
    404        {
    405            return systemID;
    406        }
    407        finally
    408        {
    409            endRead();
    410        }
    411    }
    412
    413    /**
    414     * Sets the system ID of the DOCTYPE declaration. When this configuration is
    415     * saved, a DOCTYPE declaration will be constructed that contains this
    416     * system ID.
    417     *
    418     * @param systemID the system ID
    419     * @since 1.3
    420     */
    421    public void setSystemID(final String systemID)
    422    {
    423        beginWrite(false);
    424        try
    425        {
    426            this.systemID = systemID;
    427        }
    428        finally
    429        {
    430            endWrite();
    431        }
    432    }
    433
    434    /**
    435     * Returns the value of the validating flag.
    436     *
    437     * @return the validating flag
    438     * @since 1.2
    439     */
    440    public boolean isValidating()
    441    {
    442        return validating;
    443    }
    444
    445    /**
    446     * Sets the value of the validating flag. This flag determines whether
    447     * DTD/Schema validation should be performed when loading XML documents. This
    448     * flag is evaluated only if no custom {@code DocumentBuilder} was set.
    449     *
    450     * @param validating the validating flag
    451     * @since 1.2
    452     */
    453    public void setValidating(final boolean validating)
    454    {
    455        if (!schemaValidation)
    456        {
    457            this.validating = validating;
    458        }
    459    }
    460
    461
    462    /**
    463     * Returns the value of the schemaValidation flag.
    464     *
    465     * @return the schemaValidation flag
    466     * @since 1.7
    467     */
    468    public boolean isSchemaValidation()
    469    {
    470        return schemaValidation;
    471    }
    472
    473    /**
    474     * Sets the value of the schemaValidation flag. This flag determines whether
    475     * DTD or Schema validation should be used. This
    476     * flag is evaluated only if no custom {@code DocumentBuilder} was set.
    477     * If set to true the XML document must contain a schemaLocation definition
    478     * that provides resolvable hints to the required schemas.
    479     *
    480     * @param schemaValidation the validating flag
    481     * @since 1.7
    482     */
    483    public void setSchemaValidation(final boolean schemaValidation)
    484    {
    485        this.schemaValidation = schemaValidation;
    486        if (schemaValidation)
    487        {
    488            this.validating = true;
    489        }
    490    }
    491
    492    /**
    493     * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no
    494     * effect.
    495     * @param resolver The EntityResolver to use.
    496     * @since 1.7
    497     */
    498    public void setEntityResolver(final EntityResolver resolver)
    499    {
    500        this.entityResolver = resolver;
    501    }
    502
    503    /**
    504     * Returns the EntityResolver.
    505     * @return The EntityResolver.
    506     * @since 1.7
    507     */
    508    public EntityResolver getEntityResolver()
    509    {
    510        return this.entityResolver;
    511    }
    512
    513    /**
    514     * Returns the XML document this configuration was loaded from. The return
    515     * value is <b>null</b> if this configuration was not loaded from a XML
    516     * document.
    517     *
    518     * @return the XML document this configuration was loaded from
    519     */
    520    public Document getDocument()
    521    {
    522        final XMLDocumentHelper docHelper = getDocumentHelper();
    523        return docHelper != null ? docHelper.getDocument() : null;
    524    }
    525
    526    /**
    527     * Returns the helper object for managing the underlying document.
    528     *
    529     * @return the {@code XMLDocumentHelper}
    530     */
    531    private XMLDocumentHelper getDocumentHelper()
    532    {
    533        final ReferenceNodeHandler handler = getReferenceHandler();
    534        return (XMLDocumentHelper) handler.getReference(handler.getRootNode());
    535    }
    536
    537    /**
    538     * Returns the extended node handler with support for references.
    539     *
    540     * @return the {@code ReferenceNodeHandler}
    541     */
    542    private ReferenceNodeHandler getReferenceHandler()
    543    {
    544        return getSubConfigurationParentModel().getReferenceNodeHandler();
    545    }
    546
    547    /**
    548     * Initializes this configuration from an XML document.
    549     *
    550     * @param docHelper the helper object with the document to be parsed
    551     * @param elemRefs a flag whether references to the XML elements should be set
    552     */
    553    private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs)
    554    {
    555        final Document document = docHelper.getDocument();
    556        setPublicID(docHelper.getSourcePublicID());
    557        setSystemID(docHelper.getSourceSystemID());
    558
    559        final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
    560        final MutableObject<String> rootValue = new MutableObject<>();
    561        final Map<ImmutableNode, Object> elemRefMap =
    562                elemRefs ? new HashMap<>() : null;
    563        final Map<String, String> attributes =
    564                constructHierarchy(rootBuilder, rootValue,
    565                        document.getDocumentElement(), elemRefMap, true, 0);
    566        attributes.remove(ATTR_SPACE_INTERNAL);
    567        final ImmutableNode top =
    568                rootBuilder.value(rootValue.getValue())
    569                        .addAttributes(attributes).create();
    570        getSubConfigurationParentModel().mergeRoot(top,
    571                document.getDocumentElement().getTagName(), elemRefMap,
    572                elemRefs ? docHelper : null, this);
    573    }
    574
    575    /**
    576     * Helper method for building the internal storage hierarchy. The XML
    577     * elements are transformed into node objects.
    578     *
    579     * @param node a builder for the current node
    580     * @param refValue stores the text value of the element
    581     * @param element the current XML element
    582     * @param elemRefs a map for assigning references objects to nodes; can be
    583     *        <b>null</b>, then reference objects are irrelevant
    584     * @param trim a flag whether the text content of elements should be
    585     *        trimmed; this controls the whitespace handling
    586     * @param level the current level in the hierarchy
    587     * @return a map with all attribute values extracted for the current node;
    588     *         this map also contains the value of the trim flag for this node
    589     *         under the key {@value #ATTR_SPACE}
    590     */
    591    private Map<String, String> constructHierarchy(final ImmutableNode.Builder node,
    592            final MutableObject<String> refValue, final Element element,
    593            final Map<ImmutableNode, Object> elemRefs, final boolean trim, final int level)
    594    {
    595        final boolean trimFlag = shouldTrim(element, trim);
    596        final Map<String, String> attributes = processAttributes(element);
    597        attributes.put(ATTR_SPACE_INTERNAL, String.valueOf(trimFlag));
    598        final StringBuilder buffer = new StringBuilder();
    599        final NodeList list = element.getChildNodes();
    600        boolean hasChildren = false;
    601
    602        for (int i = 0; i < list.getLength(); i++)
    603        {
    604            final org.w3c.dom.Node w3cNode = list.item(i);
    605            if (w3cNode instanceof Element)
    606            {
    607                final Element child = (Element) w3cNode;
    608                final ImmutableNode.Builder childNode = new ImmutableNode.Builder();
    609                childNode.name(child.getTagName());
    610                final MutableObject<String> refChildValue =
    611                        new MutableObject<>();
    612                final Map<String, String> attrmap =
    613                        constructHierarchy(childNode, refChildValue, child,
    614                                elemRefs, trimFlag, level + 1);
    615                final Boolean childTrim = Boolean.valueOf(attrmap.remove(ATTR_SPACE_INTERNAL));
    616                childNode.addAttributes(attrmap);
    617                final ImmutableNode newChild =
    618                        createChildNodeWithValue(node, childNode, child,
    619                                refChildValue.getValue(),
    620                                childTrim.booleanValue(), attrmap, elemRefs);
    621                if (elemRefs != null && !elemRefs.containsKey(newChild))
    622                {
    623                    elemRefs.put(newChild, child);
    624                }
    625                hasChildren = true;
    626            }
    627            else if (w3cNode instanceof Text)
    628            {
    629                final Text data = (Text) w3cNode;
    630                buffer.append(data.getData());
    631            }
    632        }
    633
    634        boolean childrenFlag = false;
    635        if (hasChildren || trimFlag)
    636        {
    637            childrenFlag = hasChildren || attributes.size() > 1;
    638        }
    639        final String text = determineValue(buffer.toString(), childrenFlag, trimFlag);
    640        if (text.length() > 0 || (!childrenFlag && level != 0))
    641        {
    642            refValue.setValue(text);
    643        }
    644        return attributes;
    645    }
    646
    647    /**
    648     * Determines the value of a configuration node. This method mainly checks
    649     * whether the text value is to be trimmed or not. This is normally defined
    650     * by the trim flag. However, if the node has children and its content is
    651     * only whitespace, then it makes no sense to store any value; this would
    652     * only scramble layout when the configuration is saved again.
    653     *
    654     * @param content the text content of this node
    655     * @param hasChildren a flag whether the node has children
    656     * @param trimFlag the trim flag
    657     * @return the value to be stored for this node
    658     */
    659    private static String determineValue(final String content, final boolean hasChildren,
    660            final boolean trimFlag)
    661    {
    662        final boolean shouldTrim =
    663                trimFlag || (StringUtils.isBlank(content) && hasChildren);
    664        return shouldTrim ? content.trim() : content;
    665    }
    666
    667    /**
    668     * Helper method for initializing the attributes of a configuration node
    669     * from the given XML element.
    670     *
    671     * @param element the current XML element
    672     * @return a map with all attribute values extracted for the current node
    673     */
    674    private static Map<String, String> processAttributes(final Element element)
    675    {
    676        final NamedNodeMap attributes = element.getAttributes();
    677        final Map<String, String> attrmap = new HashMap<>();
    678
    679        for (int i = 0; i < attributes.getLength(); ++i)
    680        {
    681            final org.w3c.dom.Node w3cNode = attributes.item(i);
    682            if (w3cNode instanceof Attr)
    683            {
    684                final Attr attr = (Attr) w3cNode;
    685                attrmap.put(attr.getName(), attr.getValue());
    686            }
    687        }
    688
    689        return attrmap;
    690    }
    691
    692    /**
    693     * Creates a new child node, assigns its value, and adds it to its parent.
    694     * This method also deals with elements whose value is a list. In this case
    695     * multiple child elements must be added. The return value is the first
    696     * child node which was added.
    697     *
    698     * @param parent the builder for the parent element
    699     * @param child the builder for the child element
    700     * @param elem the associated XML element
    701     * @param value the value of the child element
    702     * @param trim flag whether texts of elements should be trimmed
    703     * @param attrmap a map with the attributes of the current node
    704     * @param elemRefs a map for assigning references objects to nodes; can be
    705     *        <b>null</b>, then reference objects are irrelevant
    706     * @return the first child node added to the parent
    707     */
    708    private ImmutableNode createChildNodeWithValue(final ImmutableNode.Builder parent,
    709            final ImmutableNode.Builder child, final Element elem, final String value,
    710            final boolean trim, final Map<String, String> attrmap,
    711            final Map<ImmutableNode, Object> elemRefs)
    712    {
    713        ImmutableNode addedChildNode;
    714        Collection<String> values;
    715
    716        if (value != null)
    717        {
    718            values = getListDelimiterHandler().split(value, trim);
    719        }
    720        else
    721        {
    722            values = Collections.emptyList();
    723        }
    724
    725        if (values.size() > 1)
    726        {
    727            final Map<ImmutableNode, Object> refs = isSingleElementList(elem) ? elemRefs : null;
    728            final Iterator<String> it = values.iterator();
    729            // Create new node for the original child's first value
    730            child.value(it.next());
    731            addedChildNode = child.create();
    732            parent.addChild(addedChildNode);
    733            XMLListReference.assignListReference(refs, addedChildNode, elem);
    734
    735            // add multiple new children
    736            while (it.hasNext())
    737            {
    738                final ImmutableNode.Builder c = new ImmutableNode.Builder();
    739                c.name(addedChildNode.getNodeName());
    740                c.value(it.next());
    741                c.addAttributes(attrmap);
    742                final ImmutableNode newChild = c.create();
    743                parent.addChild(newChild);
    744                XMLListReference.assignListReference(refs, newChild, null);
    745            }
    746        }
    747        else if (values.size() == 1)
    748        {
    749            // we will have to replace the value because it might
    750            // contain escaped delimiters
    751            child.value(values.iterator().next());
    752            addedChildNode = child.create();
    753            parent.addChild(addedChildNode);
    754        }
    755        else
    756        {
    757            addedChildNode = child.create();
    758            parent.addChild(addedChildNode);
    759        }
    760
    761        return addedChildNode;
    762    }
    763
    764    /**
    765     * Checks whether an element defines a complete list. If this is the case,
    766     * extended list handling can be applied.
    767     *
    768     * @param element the element to be checked
    769     * @return a flag whether this is the only element defining the list
    770     */
    771    private static boolean isSingleElementList(final Element element)
    772    {
    773        final Node parentNode = element.getParentNode();
    774        return countChildElements(parentNode, element.getTagName()) == 1;
    775    }
    776
    777    /**
    778     * Determines the number of child elements of this given node with the
    779     * specified node name.
    780     *
    781     * @param parent the parent node
    782     * @param name the name in question
    783     * @return the number of child elements with this name
    784     */
    785    private static int countChildElements(final Node parent, final String name)
    786    {
    787        final NodeList childNodes = parent.getChildNodes();
    788        int count = 0;
    789        for (int i = 0; i < childNodes.getLength(); i++)
    790        {
    791            final Node item = childNodes.item(i);
    792            if (item instanceof Element)
    793            {
    794                if (name.equals(((Element) item).getTagName()))
    795                {
    796                    count++;
    797                }
    798            }
    799        }
    800        return count;
    801    }
    802
    803    /**
    804     * Checks whether the content of the current XML element should be trimmed.
    805     * This method checks whether a {@code xml:space} attribute is
    806     * present and evaluates its value. See <a
    807     * href="http://www.w3.org/TR/REC-xml/#sec-white-space">
    808     * http://www.w3.org/TR/REC-xml/#sec-white-space</a> for more details.
    809     *
    810     * @param element the current XML element
    811     * @param currentTrim the current trim flag
    812     * @return a flag whether the content of this element should be trimmed
    813     */
    814    private static boolean shouldTrim(final Element element, final boolean currentTrim)
    815    {
    816        final Attr attr = element.getAttributeNode(ATTR_SPACE);
    817
    818        if (attr == null)
    819        {
    820            return currentTrim;
    821        }
    822        return !VALUE_PRESERVE.equals(attr.getValue());
    823    }
    824
    825    /**
    826     * Creates the {@code DocumentBuilder} to be used for loading files.
    827     * This implementation checks whether a specific
    828     * {@code DocumentBuilder} has been set. If this is the case, this
    829     * one is used. Otherwise a default builder is created. Depending on the
    830     * value of the validating flag this builder will be a validating or a non
    831     * validating {@code DocumentBuilder}.
    832     *
    833     * @return the {@code DocumentBuilder} for loading configuration
    834     * files
    835     * @throws ParserConfigurationException if an error occurs
    836     * @since 1.2
    837     */
    838    protected DocumentBuilder createDocumentBuilder()
    839            throws ParserConfigurationException
    840    {
    841        if (getDocumentBuilder() != null)
    842        {
    843            return getDocumentBuilder();
    844        }
    845        final DocumentBuilderFactory factory = DocumentBuilderFactory
    846                .newInstance();
    847        if (isValidating())
    848        {
    849            factory.setValidating(true);
    850            if (isSchemaValidation())
    851            {
    852                factory.setNamespaceAware(true);
    853                factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
    854            }
    855        }
    856
    857        final DocumentBuilder result = factory.newDocumentBuilder();
    858        result.setEntityResolver(this.entityResolver);
    859
    860        if (isValidating())
    861        {
    862            // register an error handler which detects validation errors
    863            result.setErrorHandler(new DefaultHandler()
    864            {
    865                @Override
    866                public void error(final SAXParseException ex) throws SAXException
    867                {
    868                    throw ex;
    869                }
    870            });
    871        }
    872        return result;
    873    }
    874
    875    /**
    876     * Creates and initializes the transformer used for save operations. This
    877     * base implementation initializes all of the default settings like
    878     * indentation mode and the DOCTYPE. Derived classes may overload this method
    879     * if they have specific needs.
    880     *
    881     * @return the transformer to use for a save operation
    882     * @throws ConfigurationException if an error occurs
    883     * @since 1.3
    884     */
    885    protected Transformer createTransformer() throws ConfigurationException
    886    {
    887        final Transformer transformer = XMLDocumentHelper.createTransformer();
    888
    889        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    890        transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, Integer.toString(DEFAULT_INDENT_SIZE));
    891        if (locator != null && locator.getEncoding() != null)
    892        {
    893            transformer.setOutputProperty(OutputKeys.ENCODING, locator.getEncoding());
    894        }
    895        if (publicID != null)
    896        {
    897            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicID);
    898        }
    899        if (systemID != null)
    900        {
    901            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemID);
    902        }
    903
    904        return transformer;
    905    }
    906
    907    /**
    908     * Creates a DOM document from the internal tree of configuration nodes.
    909     *
    910     * @return the new document
    911     * @throws ConfigurationException if an error occurs
    912     */
    913    private Document createDocument() throws ConfigurationException
    914    {
    915        final ReferenceNodeHandler handler = getReferenceHandler();
    916        final XMLDocumentHelper docHelper =
    917                (XMLDocumentHelper) handler.getReference(handler.getRootNode());
    918        final XMLDocumentHelper newHelper =
    919                docHelper == null ? XMLDocumentHelper
    920                        .forNewDocument(getRootElementName()) : docHelper
    921                        .createCopy();
    922
    923        final XMLBuilderVisitor builder =
    924                new XMLBuilderVisitor(newHelper, getListDelimiterHandler());
    925        builder.handleRemovedNodes(handler);
    926        builder.processDocument(handler);
    927        initRootElementText(newHelper.getDocument(), getModel()
    928                .getNodeHandler().getRootNode().getValue());
    929        return newHelper.getDocument();
    930    }
    931
    932    /**
    933     * Sets the text of the root element of a newly created XML Document.
    934     *
    935     * @param doc the document
    936     * @param value the new text to be set
    937     */
    938    private void initRootElementText(final Document doc, final Object value)
    939    {
    940        final Element elem = doc.getDocumentElement();
    941        final NodeList children = elem.getChildNodes();
    942
    943        // Remove all existing text nodes
    944        for (int i = 0; i < children.getLength(); i++)
    945        {
    946            final org.w3c.dom.Node nd = children.item(i);
    947            if (nd.getNodeType() == org.w3c.dom.Node.TEXT_NODE)
    948            {
    949                elem.removeChild(nd);
    950            }
    951        }
    952
    953        if (value != null)
    954        {
    955            // Add a new text node
    956            elem.appendChild(doc.createTextNode(String.valueOf(value)));
    957        }
    958    }
    959
    960    /**
    961     * {@inheritDoc} Stores the passed in locator for the upcoming IO operation.
    962     */
    963    @Override
    964    public void initFileLocator(final FileLocator loc)
    965    {
    966        locator = loc;
    967    }
    968
    969    /**
    970     * Loads the configuration from the given reader.
    971     * Note that the {@code clear()} method is not called, so
    972     * the properties contained in the loaded file will be added to the
    973     * current set of properties.
    974     *
    975     * @param in the reader
    976     * @throws ConfigurationException if an error occurs
    977     * @throws IOException if an IO error occurs
    978     */
    979    @Override
    980    public void read(final Reader in) throws ConfigurationException, IOException
    981    {
    982        load(new InputSource(in));
    983    }
    984
    985    /**
    986     * Loads the configuration from the given input stream. This is analogous to
    987     * {@link #read(Reader)}, but data is read from a stream. Note that this
    988     * method will be called most time when reading an XML configuration source.
    989     * By reading XML documents directly from an input stream, the file's
    990     * encoding can be correctly dealt with.
    991     *
    992     * @param in the input stream
    993     * @throws ConfigurationException if an error occurs
    994     * @throws IOException if an IO error occurs
    995     */
    996    @Override
    997    public void read(final InputStream in) throws ConfigurationException, IOException
    998    {
    999        load(new InputSource(in));
    1000    }
    1001
    1002    /**
    1003     * Loads a configuration file from the specified input source.
    1004     *
    1005     * @param source the input source
    1006     * @throws ConfigurationException if an error occurs
    1007     */
    1008    private void load(final InputSource source) throws ConfigurationException
    1009    {
    1010        if (locator == null)
    1011        {
    1012            throw new ConfigurationException("Load operation not properly "
    1013                    + "initialized! Do not call read(InputStream) directly,"
    1014                    + " but use a FileHandler to load a configuration.");
    1015        }
    1016
    1017        try
    1018        {
    1019            final URL sourceURL = locator.getSourceURL();
    1020            if (sourceURL != null)
    1021            {
    1022                source.setSystemId(sourceURL.toString());
    1023            }
    1024
    1025            final DocumentBuilder builder = createDocumentBuilder();
    1026            final Document newDocument = builder.parse(source);
    1027            final Document oldDocument = getDocument();
    1028            initProperties(XMLDocumentHelper.forSourceDocument(newDocument),
    1029                    oldDocument == null);
    1030        }
    1031        catch (final SAXParseException spe)
    1032        {
    1033            throw new ConfigurationException("Error parsing " + source.getSystemId(), spe);
    1034        }
    1035        catch (final Exception e)
    1036        {
    1037            this.getLogger().debug("Unable to load the configuration: " + e);
    1038            throw new ConfigurationException("Unable to load the configuration", e);
    1039        }
    1040    }
    1041
    1042    /**
    1043     * Saves the configuration to the specified writer.
    1044     *
    1045     * @param writer the writer used to save the configuration
    1046     * @throws ConfigurationException if an error occurs
    1047     * @throws IOException if an IO error occurs
    1048     */
    1049    @Override
    1050    public void write(final Writer writer) throws ConfigurationException, IOException
    1051    {
    1052        write(writer, createTransformer());
    1053    }
    1054
    1055    /**
    1056     * Saves the configuration to the specified writer.
    1057     *
    1058     * @param writer the writer used to save the configuration.
    1059     * @param transformer How to transform this configuration.
    1060     * @throws ConfigurationException if an error occurs.
    1061     * @since 2.7.0
    1062     */
    1063    public void write(final Writer writer, final Transformer transformer) throws ConfigurationException
    1064    {
    1065        final Source source = new DOMSource(createDocument());
    1066        final Result result = new StreamResult(writer);
    1067        XMLDocumentHelper.transform(transformer, source, result);
    1068    }
    1069
    1070    /**
    1071     * Validate the document against the Schema.
    1072     * @throws ConfigurationException if the validation fails.
    1073     */
    1074    public void validate() throws ConfigurationException
    1075    {
    1076        beginWrite(false);
    1077        try
    1078        {
    1079            final Transformer transformer = createTransformer();
    1080            final Source source = new DOMSource(createDocument());
    1081            final StringWriter writer = new StringWriter();
    1082            final Result result = new StreamResult(writer);
    1083            XMLDocumentHelper.transform(transformer, source, result);
    1084            final Reader reader = new StringReader(writer.getBuffer().toString());
    1085            final DocumentBuilder builder = createDocumentBuilder();
    1086            builder.parse(new InputSource(reader));
    1087        }
    1088        catch (final SAXException e)
    1089        {
    1090            throw new ConfigurationException("Validation failed", e);
    1091        }
    1092        catch (final IOException e)
    1093        {
    1094            throw new ConfigurationException("Validation failed", e);
    1095        }
    1096        catch (final ParserConfigurationException pce)
    1097        {
    1098            throw new ConfigurationException("Validation failed", pce);
    1099        }
    1100        finally
    1101        {
    1102            endWrite();
    1103        }
    1104    }
    1105
    1106    /**
    1107     * A concrete {@code BuilderVisitor} that can construct XML
    1108     * documents.
    1109     */
    1110    static class XMLBuilderVisitor extends BuilderVisitor
    1111    {
    1112        /** Stores the document to be constructed. */
    1113        private final Document document;
    1114
    1115        /** The element mapping. */
    1116        private final Map<Node, Node> elementMapping;
    1117
    1118        /** A mapping for the references for new nodes. */
    1119        private final Map<ImmutableNode, Element> newElements;
    1120
    1121        /** Stores the list delimiter handler .*/
    1122        private final ListDelimiterHandler listDelimiterHandler;
    1123
    1124        /**
    1125         * Creates a new instance of {@code XMLBuilderVisitor}.
    1126         *
    1127         * @param docHelper the document helper
    1128         * @param handler the delimiter handler for properties with multiple
    1129         *        values
    1130         */
    1131        public XMLBuilderVisitor(final XMLDocumentHelper docHelper,
    1132                final ListDelimiterHandler handler)
    1133        {
    1134            document = docHelper.getDocument();
    1135            elementMapping = docHelper.getElementMapping();
    1136            listDelimiterHandler = handler;
    1137            newElements = new HashMap<>();
    1138        }
    1139
    1140        /**
    1141         * Processes the specified document, updates element values, and adds
    1142         * new nodes to the hierarchy.
    1143         *
    1144         * @param refHandler the {@code ReferenceNodeHandler}
    1145         */
    1146        public void processDocument(final ReferenceNodeHandler refHandler)
    1147        {
    1148            updateAttributes(refHandler.getRootNode(), document.getDocumentElement());
    1149            NodeTreeWalker.INSTANCE.walkDFS(refHandler.getRootNode(), this,
    1150                    refHandler);
    1151        }
    1152
    1153        /**
    1154         * Updates the current XML document regarding removed nodes. The
    1155         * elements associated with removed nodes are removed from the document.
    1156         *
    1157         * @param refHandler the {@code ReferenceNodeHandler}
    1158         */
    1159        public void handleRemovedNodes(final ReferenceNodeHandler refHandler)
    1160        {
    1161            for (final Object ref : refHandler.removedReferences())
    1162            {
    1163                if (ref instanceof Node)
    1164                {
    1165                    final Node removedElem = (Node) ref;
    1166                    removeReference((Element) elementMapping.get(removedElem));
    1167                }
    1168            }
    1169        }
    1170
    1171        /**
    1172         * {@inheritDoc} This implementation ensures that the correct XML
    1173         * element is created and inserted between the given siblings.
    1174         */
    1175        @Override
    1176        protected void insert(final ImmutableNode newNode, final ImmutableNode parent,
    1177                final ImmutableNode sibling1, final ImmutableNode sibling2,
    1178                final ReferenceNodeHandler refHandler)
    1179        {
    1180            if (XMLListReference.isListNode(newNode, refHandler))
    1181            {
    1182                return;
    1183            }
    1184
    1185            final Element elem = document.createElement(newNode.getNodeName());
    1186            newElements.put(newNode, elem);
    1187            updateAttributes(newNode, elem);
    1188            if (newNode.getValue() != null)
    1189            {
    1190                final String txt =
    1191                        String.valueOf(listDelimiterHandler.escape(
    1192                                newNode.getValue(),
    1193                                ListDelimiterHandler.NOOP_TRANSFORMER));
    1194                elem.appendChild(document.createTextNode(txt));
    1195            }
    1196            if (sibling2 == null)
    1197            {
    1198                getElement(parent, refHandler).appendChild(elem);
    1199            }
    1200            else if (sibling1 != null)
    1201            {
    1202                getElement(parent, refHandler).insertBefore(elem,
    1203                        getElement(sibling1, refHandler).getNextSibling());
    1204            }
    1205            else
    1206            {
    1207                getElement(parent, refHandler).insertBefore(elem,
    1208                        getElement(parent, refHandler).getFirstChild());
    1209            }
    1210        }
    1211
    1212        /**
    1213         * {@inheritDoc} This implementation determines the XML element
    1214         * associated with the given node. Then this element's value and
    1215         * attributes are set accordingly.
    1216         */
    1217        @Override
    1218        protected void update(final ImmutableNode node, final Object reference,
    1219                final ReferenceNodeHandler refHandler)
    1220        {
    1221            if (XMLListReference.isListNode(node, refHandler))
    1222            {
    1223                if (XMLListReference.isFirstListItem(node, refHandler))
    1224                {
    1225                    final String value = XMLListReference.listValue(node, refHandler, listDelimiterHandler);
    1226                    updateElement(node, refHandler, value);
    1227                }
    1228            }
    1229            else
    1230            {
    1231                final Object value = listDelimiterHandler.escape(refHandler.getValue(node),
    1232                        ListDelimiterHandler.NOOP_TRANSFORMER);
    1233                updateElement(node, refHandler, value);
    1234            }
    1235        }
    1236
    1237        private void updateElement(final ImmutableNode node, final ReferenceNodeHandler refHandler,
    1238                                   final Object value)
    1239        {
    1240            final Element element = getElement(node, refHandler);
    1241            updateElement(element, value);
    1242            updateAttributes(node, element);
    1243        }
    1244
    1245        /**
    1246         * Updates the node's value if it represents an element node.
    1247         *
    1248         * @param element the element
    1249         * @param value the new value
    1250         */
    1251        private void updateElement(final Element element, final Object value)
    1252        {
    1253            Text txtNode = findTextNodeForUpdate(element);
    1254            if (value == null)
    1255            {
    1256                // remove text
    1257                if (txtNode != null)
    1258                {
    1259                    element.removeChild(txtNode);
    1260                }
    1261            }
    1262            else
    1263            {
    1264                final String newValue = String.valueOf(value);
    1265                if (txtNode == null)
    1266                {
    1267                    txtNode = document.createTextNode(newValue);
    1268                    if (element.getFirstChild() != null)
    1269                    {
    1270                        element.insertBefore(txtNode, element.getFirstChild());
    1271                    }
    1272                    else
    1273                    {
    1274                        element.appendChild(txtNode);
    1275                    }
    1276                }
    1277                else
    1278                {
    1279                    txtNode.setNodeValue(newValue);
    1280                }
    1281            }
    1282        }
    1283
    1284        /**
    1285         * Updates the associated XML elements when a node is removed.
    1286         * @param element the element to be removed
    1287         */
    1288        private void removeReference(final Element element)
    1289        {
    1290            final org.w3c.dom.Node parentElem = element.getParentNode();
    1291            if (parentElem != null)
    1292            {
    1293                parentElem.removeChild(element);
    1294            }
    1295        }
    1296
    1297        /**
    1298         * Helper method for accessing the element of the specified node.
    1299         *
    1300         * @param node the node
    1301         * @param refHandler the {@code ReferenceNodeHandler}
    1302         * @return the element of this node
    1303         */
    1304        private Element getElement(final ImmutableNode node,
    1305                final ReferenceNodeHandler refHandler)
    1306        {
    1307            final Element elementNew = newElements.get(node);
    1308            if (elementNew != null)
    1309            {
    1310                return elementNew;
    1311            }
    1312
    1313            // special treatment for root node of the hierarchy
    1314            final Object reference = refHandler.getReference(node);
    1315            Node element;
    1316            if (reference instanceof XMLDocumentHelper)
    1317            {
    1318                element =
    1319                        ((XMLDocumentHelper) reference).getDocument()
    1320                                .getDocumentElement();
    1321            }
    1322            else if (reference instanceof XMLListReference)
    1323            {
    1324                element = ((XMLListReference) reference).getElement();
    1325            }
    1326            else
    1327            {
    1328                element = (Node) reference;
    1329            }
    1330            return element != null ? (Element) elementMapping.get(element)
    1331                    : document.getDocumentElement();
    1332        }
    1333
    1334        /**
    1335         * Helper method for updating the values of all attributes of the
    1336         * specified node.
    1337         *
    1338         * @param node the affected node
    1339         * @param elem the element that is associated with this node
    1340         */
    1341        private static void updateAttributes(final ImmutableNode node, final Element elem)
    1342        {
    1343            if (node != null && elem != null)
    1344            {
    1345                clearAttributes(elem);
    1346                for (final Map.Entry<String, Object> e : node.getAttributes()
    1347                        .entrySet())
    1348                {
    1349                    if (e.getValue() != null)
    1350                    {
    1351                        elem.setAttribute(e.getKey(), e.getValue().toString());
    1352                    }
    1353                }
    1354            }
    1355        }
    1356
    1357        /**
    1358         * Removes all attributes of the given element.
    1359         *
    1360         * @param elem the element
    1361         */
    1362        private static void clearAttributes(final Element elem)
    1363        {
    1364            final NamedNodeMap attributes = elem.getAttributes();
    1365            for (int i = 0; i < attributes.getLength(); i++)
    1366            {
    1367                elem.removeAttribute(attributes.item(i).getNodeName());
    1368            }
    1369        }
    1370
    1371        /**
    1372         * Returns the only text node of an element for update. This method is
    1373         * called when the element's text changes. Then all text nodes except
    1374         * for the first are removed. A reference to the first is returned or
    1375         * <b>null</b> if there is no text node at all.
    1376         *
    1377         * @param elem the element
    1378         * @return the first and only text node
    1379         */
    1380        private static Text findTextNodeForUpdate(final Element elem)
    1381        {
    1382            Text result = null;
    1383            // Find all Text nodes
    1384            final NodeList children = elem.getChildNodes();
    1385            final Collection<org.w3c.dom.Node> textNodes =
    1386                    new ArrayList<>();
    1387            for (int i = 0; i < children.getLength(); i++)
    1388            {
    1389                final org.w3c.dom.Node nd = children.item(i);
    1390                if (nd instanceof Text)
    1391                {
    1392                    if (result == null)
    1393                    {
    1394                        result = (Text) nd;
    1395                    }
    1396                    else
    1397                    {
    1398                        textNodes.add(nd);
    1399                    }
    1400                }
    1401            }
    1402
    1403            // We don't want CDATAs
    1404            if (result instanceof CDATASection)
    1405            {
    1406                textNodes.add(result);
    1407                result = null;
    1408            }
    1409
    1410            // Remove all but the first Text node
    1411            for (final org.w3c.dom.Node tn : textNodes)
    1412            {
    1413                elem.removeChild(tn);
    1414            }
    1415            return result;
    1416        }
    1417    }
    1418}