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