View Javadoc

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