View Javadoc

1   /*
2    * Copyright 2004-2005 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.apache.commons.configuration;
18  
19  import java.io.File;
20  import java.io.InputStream;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Iterator;
27  import java.util.List;
28  
29  import javax.xml.parsers.DocumentBuilder;
30  import javax.xml.parsers.DocumentBuilderFactory;
31  import javax.xml.parsers.ParserConfigurationException;
32  import javax.xml.transform.OutputKeys;
33  import javax.xml.transform.Result;
34  import javax.xml.transform.Source;
35  import javax.xml.transform.Transformer;
36  import javax.xml.transform.TransformerException;
37  import javax.xml.transform.TransformerFactory;
38  import javax.xml.transform.dom.DOMSource;
39  import javax.xml.transform.stream.StreamResult;
40  
41  import org.w3c.dom.Attr;
42  import org.w3c.dom.CDATASection;
43  import org.w3c.dom.DOMException;
44  import org.w3c.dom.Document;
45  import org.w3c.dom.Element;
46  import org.w3c.dom.NamedNodeMap;
47  import org.w3c.dom.NodeList;
48  import org.w3c.dom.Text;
49  import org.xml.sax.InputSource;
50  import org.xml.sax.SAXException;
51  import org.xml.sax.SAXParseException;
52  import org.xml.sax.helpers.DefaultHandler;
53  
54  /***
55   * <p>A specialized hierarchical configuration class that is able to parse XML
56   * documents.</p>
57   *
58   * <p>The parsed document will be stored keeping its structure. The class also
59   * tries to preserve as much information from the loaded XML document as
60   * possible, including comments and processing instructions. These will be
61   * contained in documents created by the <code>save()</code> methods, too.</p>
62   *
63   * <p>Like other file based configuration classes this class maintains the name
64   * and path to the loaded configuration file. These properties can be altered
65   * using several setter methods, but they are not modified by <code>save()</code>
66   * and <code>load()</code> methods. If XML documents contain relative paths to
67   * other documents (e.g. to a DTD), these references are resolved based on the
68   * path set for this configuration.</p>
69   *
70   * <p>By inheriting from <code>{@link AbstractConfiguration}</code> this class
71   * provides some extended functionaly, e.g. interpolation of property values.
72   * Like in <code>{@link PropertiesConfiguration}</code> property values can
73   * contain delimiter characters (the comma ',' per default) and are then splitted
74   * into multiple values. This works for XML attributes and text content of
75   * elements as well. The delimiter can be escaped by a backslash. As an example
76   * consider the following XML fragment:</p>
77   *
78   * <p>
79   * <pre>
80   * &lt;config&gt;
81   *   &lt;array&gt;10,20,30,40&lt;/array&gt;
82   *   &lt;scalar&gt;3\,1415&lt;/scalar&gt;
83   *   &lt;cite text="To be or not to be\, this is the question!"/&gt;
84   * &lt;/config&gt;
85   * </pre>
86   * </p>
87   * <p>Here the content of the <code>array</code> element will be splitted at
88   * the commas, so the <code>array</code> key will be assigned 4 values. In the
89   * <code>scalar</code> property and the <code>text</code> attribute of the
90   * <code>cite</code> element the comma is escaped, so that no splitting is
91   * performed.</p>
92   *
93   * <p><code>XMLConfiguration</code> implements the <code>{@link FileConfiguration}</code>
94   * interface and thus provides full support for loading XML documents from
95   * different sources like files, URLs, or streams. A full description of these
96   * features can be found in the documentation of
97   * <code>{@link AbstractFileConfiguration}</code>.</p>
98   *
99   * @since commons-configuration 1.0
100  *
101  * @author J&ouml;rg Schaible
102  * @author <a href="mailto:oliver.heger@t-online.de">Oliver Heger </a>
103  * @version $Revision$, $Date: 2005-11-20 20:39:51 +0100 (Sun, 20 Nov 2005) $
104  */
105 public class XMLConfiguration extends AbstractHierarchicalFileConfiguration
106 {
107     /*** Constant for the default root element name. */
108     private static final String DEFAULT_ROOT_NAME = "configuration";
109 
110     /*** The document from this configuration's data source. */
111     private Document document;
112 
113     /*** Stores the name of the root element. */
114     private String rootElementName;
115 
116     /*** Stores the document builder that should be used for loading.*/
117     private DocumentBuilder documentBuilder;
118 
119     /*** Stores a flag whether DTD validation should be performed.*/
120     private boolean validating;
121 
122     /***
123      * Creates a new instance of <code>XMLConfiguration</code>.
124      */
125     public XMLConfiguration()
126     {
127         super();
128     }
129 
130     /***
131      * Creates a new instance of <code>XMLConfiguration</code>.
132      * The configuration is loaded from the specified file
133      *
134      * @param fileName the name of the file to load
135      * @throws ConfigurationException if the file cannot be loaded
136      */
137     public XMLConfiguration(String fileName) throws ConfigurationException
138     {
139         this();
140         setFileName(fileName);
141         load();
142     }
143 
144     /***
145      * Creates a new instance of <code>XMLConfiguration</code>.
146      * The configuration is loaded from the specified file.
147      *
148      * @param file the file
149      * @throws ConfigurationException if an error occurs while loading the file
150      */
151     public XMLConfiguration(File file) throws ConfigurationException
152     {
153         this();
154         setFile(file);
155         if (file.exists())
156         {
157             load();
158         }
159     }
160 
161     /***
162      * Creates a new instance of <code>XMLConfiguration</code>.
163      * The configuration is loaded from the specified URL.
164      *
165      * @param url the URL
166      * @throws ConfigurationException if loading causes an error
167      */
168     public XMLConfiguration(URL url) throws ConfigurationException
169     {
170         this();
171         setURL(url);
172         load();
173     }
174 
175     /***
176      * Returns the name of the root element. If this configuration was loaded
177      * from a XML document, the name of this document's root element is
178      * returned. Otherwise it is possible to set a name for the root element
179      * that will be used when this configuration is stored.
180      *
181      * @return the name of the root element
182      */
183     public String getRootElementName()
184     {
185         if (getDocument() == null)
186         {
187             return (rootElementName == null) ? DEFAULT_ROOT_NAME : rootElementName;
188         }
189         else
190         {
191             return getDocument().getDocumentElement().getNodeName();
192         }
193     }
194 
195     /***
196      * Sets the name of the root element. This name is used when this
197      * configuration object is stored in an XML file. Note that setting the name
198      * of the root element works only if this configuration has been newly
199      * created. If the configuration was loaded from an XML file, the name
200      * cannot be changed and an <code>UnsupportedOperationException</code>
201      * exception is thrown. Whether this configuration has been loaded from an
202      * XML document or not can be found out using the <code>getDocument()</code>
203      * method.
204      *
205      * @param name the name of the root element
206      */
207     public void setRootElementName(String name)
208     {
209         if (getDocument() != null)
210         {
211             throw new UnsupportedOperationException("The name of the root element "
212                     + "cannot be changed when loaded from an XML document!");
213         }
214         rootElementName = name;
215     }
216 
217     /***
218      * Returns the <code>DocumentBuilder</code> object that is used for
219      * loading documents. If no specific builder has been set, this method
220      * returns <b>null</b>.
221      *
222      * @return the <code>DocumentBuilder</code> for loading new documents
223      * @since 1.2
224      */
225     public DocumentBuilder getDocumentBuilder()
226     {
227         return documentBuilder;
228     }
229 
230     /***
231      * Sets the <code>DocumentBuilder</code> object to be used for loading
232      * documents. This method makes it possible to specify the exact document
233      * builder. So an application can create a builder, configure it for its
234      * special needs, and then pass it to this method.
235      *
236      * @param documentBuilder the document builder to be used; if undefined, a
237      * default builder will be used
238      * @since 1.2
239      */
240     public void setDocumentBuilder(DocumentBuilder documentBuilder)
241     {
242         this.documentBuilder = documentBuilder;
243     }
244 
245     /***
246      * Returns the value of the validating flag.
247      *
248      * @return the validating flag
249      * @since 1.2
250      */
251     public boolean isValidating()
252     {
253         return validating;
254     }
255 
256     /***
257      * Sets the value of the validating flag. This flag determines whether
258      * DTD validation should be performed when loading XML documents. This
259      * flag is evaluated only if no custom <code>DocumentBuilder</code> was set.
260      *
261      * @param validating the validating flag
262      * @since 1.2
263      */
264     public void setValidating(boolean validating)
265     {
266         this.validating = validating;
267     }
268 
269     /***
270      * Returns the XML document this configuration was loaded from. The return
271      * value is <b>null</b> if this configuration was not loaded from a XML
272      * document.
273      *
274      * @return the XML document this configuration was loaded from
275      */
276     public Document getDocument()
277     {
278         return document;
279     }
280 
281     /***
282      * Removes all properties from this configuration. If this configuration
283      * was loaded from a file, the associated DOM document is also cleared.
284      */
285     public void clear()
286     {
287         super.clear();
288         document = null;
289     }
290 
291     /***
292      * Initializes this configuration from an XML document.
293      *
294      * @param document the document to be parsed
295      * @param elemRefs a flag whether references to the XML elements should be set
296      */
297     public void initProperties(Document document, boolean elemRefs)
298     {
299         constructHierarchy(getRoot(), document.getDocumentElement(), elemRefs);
300     }
301 
302     /***
303      * Helper method for building the internal storage hierarchy. The XML
304      * elements are transformed into node objects.
305      *
306      * @param node the actual node
307      * @param element the actual XML element
308      * @param elemRefs a flag whether references to the XML elements should be set
309      */
310     private void constructHierarchy(Node node, Element element, boolean elemRefs)
311     {
312         processAttributes(node, element, elemRefs);
313         StringBuffer buffer = new StringBuffer();
314         NodeList list = element.getChildNodes();
315         for (int i = 0; i < list.getLength(); i++)
316         {
317             org.w3c.dom.Node w3cNode = list.item(i);
318             if (w3cNode instanceof Element)
319             {
320                 Element child = (Element) w3cNode;
321                 Node childNode = new XMLNode(child.getTagName(),
322                         elemRefs ? child : null);
323                 constructHierarchy(childNode, child, elemRefs);
324                 node.addChild(childNode);
325                 handleDelimiters(node, childNode);
326             }
327             else if (w3cNode instanceof Text)
328             {
329                 Text data = (Text) w3cNode;
330                 buffer.append(data.getData());
331             }
332         }
333         String text = buffer.toString().trim();
334         if (text.length() > 0 || !node.hasChildren())
335         {
336             node.setValue(text);
337         }
338     }
339 
340     /***
341      * Helper method for constructing node objects for the attributes of the
342      * given XML element.
343      *
344      * @param node the actual node
345      * @param element the actual XML element
346      * @param elemRefs a flag whether references to the XML elements should be set
347      */
348     private void processAttributes(Node node, Element element, boolean elemRefs)
349     {
350         NamedNodeMap attributes = element.getAttributes();
351         for (int i = 0; i < attributes.getLength(); ++i)
352         {
353             org.w3c.dom.Node w3cNode = attributes.item(i);
354             if (w3cNode instanceof Attr)
355             {
356                 Attr attr = (Attr) w3cNode;
357                 for (Iterator it = PropertyConverter.split(attr.getValue(), getDelimiter()).iterator(); it.hasNext();)
358                 {
359                     Node child = new XMLNode(ConfigurationKey.constructAttributeKey(attr.getName()),
360                             elemRefs ? element : null);
361                     child.setValue(it.next());
362                     node.addChild(child);
363                 }
364             }
365         }
366     }
367 
368     /***
369      * Deals with elements whose value is a list. In this case multiple child
370      * elements must be added.
371      *
372      * @param parent the parent element
373      * @param child the child element
374      */
375     private void handleDelimiters(Node parent, Node child)
376     {
377         if (child.getValue() != null)
378         {
379             List values = PropertyConverter.split(child.getValue().toString(),
380                     getDelimiter());
381             if (values.size() > 1)
382             {
383                 // remove the original child
384                 parent.remove(child);
385                 // add multiple new children
386                 for (Iterator it = values.iterator(); it.hasNext();)
387                 {
388                     Node c = new XMLNode(child.getName(), null);
389                     c.setValue(it.next());
390                     parent.addChild(c);
391                 }
392             }
393             else if (values.size() == 1)
394             {
395                 // we will have to replace the value because it might
396                 // contain escaped delimiters
397                 child.setValue(values.get(0));
398             }
399         }
400     }
401 
402     /***
403      * Creates the <code>DocumentBuilder</code> to be used for loading files.
404      * This implementation checks whether a specific
405      * <code>DocumentBuilder</code> has been set. If this is the case, this
406      * one is used. Otherwise a default builder is created. Depending on the
407      * value of the validating flag this builder will be a validating or a non
408      * validating <code>DocumentBuilder</code>.
409      *
410      * @return the <code>DocumentBuilder</code> for loading configuration
411      * files
412      * @throws ParserConfigurationException if an error occurs
413      * @since 1.2
414      */
415     protected DocumentBuilder createDocumentBuilder()
416             throws ParserConfigurationException
417     {
418         if (getDocumentBuilder() != null)
419         {
420             return getDocumentBuilder();
421         }
422         else
423         {
424             DocumentBuilderFactory factory = DocumentBuilderFactory
425                     .newInstance();
426             factory.setValidating(isValidating());
427             DocumentBuilder result = factory.newDocumentBuilder();
428 
429             if (isValidating())
430             {
431                 // register an error handler which detects validation errors
432                 result.setErrorHandler(new DefaultHandler()
433                 {
434                     public void error(SAXParseException ex) throws SAXException
435                     {
436                         throw ex;
437                     }
438                 });
439             }
440             return result;
441         }
442     }
443 
444     /***
445      * Creates a DOM document from the internal tree of configuration nodes.
446      *
447      * @return the new document
448      * @throws ConfigurationException if an error occurs
449      */
450     protected Document createDocument() throws ConfigurationException
451     {
452         try
453         {
454             if (document == null)
455             {
456                 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
457                 Document newDocument = builder.newDocument();
458                 Element rootElem = newDocument.createElement(getRootElementName());
459                 newDocument.appendChild(rootElem);
460                 document = newDocument;
461             }
462 
463             XMLBuilderVisitor builder = new XMLBuilderVisitor(document);
464             builder.processDocument(getRoot());
465             return document;
466         } /* try */
467         catch (DOMException domEx)
468         {
469             throw new ConfigurationException(domEx);
470         }
471         catch (ParserConfigurationException pex)
472         {
473             throw new ConfigurationException(pex);
474         }
475     }
476 
477     /***
478      * Creates a new node object. This implementation returns an instance of the
479      * <code>XMLNode</code> class.
480      *
481      * @param name the node's name
482      * @return the new node
483      */
484     protected Node createNode(String name)
485     {
486         return new XMLNode(name, null);
487     }
488 
489     /***
490      * Loads the configuration from the given input stream.
491      *
492      * @param in the input stream
493      * @throws ConfigurationException if an error occurs
494      */
495     public void load(InputStream in) throws ConfigurationException
496     {
497         load(new InputSource(in));
498     }
499 
500     /***
501      * Load the configuration from the given reader.
502      * Note that the <code>clear()</code> method is not called, so
503      * the properties contained in the loaded file will be added to the
504      * actual set of properties.
505      *
506      * @param in An InputStream.
507      *
508      * @throws ConfigurationException if an error occurs
509      */
510     public void load(Reader in) throws ConfigurationException
511     {
512         load(new InputSource(in));
513     }
514 
515     /***
516      * Loads a configuration file from the specified input source.
517      * @param source the input source
518      * @throws ConfigurationException if an error occurs
519      */
520     private void load(InputSource source) throws ConfigurationException
521     {
522         try
523         {
524             URL sourceURL = getDelegate().getURL();
525             if (sourceURL != null)
526             {
527                 source.setSystemId(sourceURL.toString());
528             }
529 
530             DocumentBuilder builder = createDocumentBuilder();
531             Document newDocument = builder.parse(source);
532             Document oldDocument = document;
533             document = null;
534             initProperties(newDocument, oldDocument == null);
535             document = (oldDocument == null) ? newDocument : oldDocument;
536         }
537         catch (Exception e)
538         {
539             throw new ConfigurationException(e.getMessage(), e);
540         }
541     }
542 
543     /***
544      * Saves the configuration to the specified writer.
545      *
546      * @param writer the writer used to save the configuration
547      * @throws ConfigurationException if an error occurs
548      */
549     public void save(Writer writer) throws ConfigurationException
550     {
551         try
552         {
553             Transformer transformer = TransformerFactory.newInstance().newTransformer();
554             Source source = new DOMSource(createDocument());
555             Result result = new StreamResult(writer);
556 
557             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
558             if (getEncoding() != null)
559             {
560                 transformer.setOutputProperty(OutputKeys.ENCODING, getEncoding());
561             }
562             transformer.transform(source, result);
563         }
564         catch (TransformerException e)
565         {
566             throw new ConfigurationException(e.getMessage(), e);
567         }
568     }
569 
570     /***
571      * Creates a copy of this object. The new configuration object will contain
572      * the same properties as the original, but it will lose any connection to a
573      * source document (if one exists). This is to avoid race conditions if both
574      * the original and the copy are modified and then saved.
575      *
576      * @return the copy
577      */
578     public Object clone()
579     {
580         XMLConfiguration copy = (XMLConfiguration) super.clone();
581 
582         // clear document related properties
583         copy.document = null;
584         copy.setDelegate(createDelegate());
585         // clear all references in the nodes, too
586         copy.getRoot().visit(new NodeVisitor()
587         {
588             public void visitBeforeChildren(Node node, ConfigurationKey key)
589             {
590                 node.setReference(null);
591             }
592         }, null);
593 
594         return copy;
595     }
596 
597     /***
598      * Creates the file configuration delegate for this object. This implementation
599      * will return an instance of a class derived from <code>FileConfigurationDelegate</code>
600      * that deals with some specialities of <code>XMLConfiguration</code>.
601      * @return the delegate for this object
602      */
603     protected FileConfigurationDelegate createDelegate()
604     {
605         return new XMLFileConfigurationDelegate();
606     }
607 
608     /***
609      * A specialized <code>Node</code> class that is connected with an XML
610      * element. Changes on a node are also performed on the associated element.
611      */
612     class XMLNode extends Node
613     {
614         /***
615          * Creates a new instance of <code>XMLNode</code> and initializes it
616          * with a name and the corresponding XML element.
617          *
618          * @param name the node's name
619          * @param elem the XML element
620          */
621         public XMLNode(String name, Element elem)
622         {
623             super(name);
624             setReference(elem);
625         }
626 
627         /***
628          * Sets the value of this node. If this node is associated with an XML
629          * element, this element will be updated, too.
630          *
631          * @param value the node's new value
632          */
633         public void setValue(Object value)
634         {
635             super.setValue(value);
636 
637             if (getReference() != null && document != null)
638             {
639                 if (ConfigurationKey.isAttributeKey(getName()))
640                 {
641                     updateAttribute();
642                 }
643                 else
644                 {
645                     updateElement(value);
646                 }
647             }
648         }
649 
650         /***
651          * Updates the associated XML elements when a node is removed.
652          */
653         protected void removeReference()
654         {
655             if (getReference() != null)
656             {
657                 Element element = (Element) getReference();
658                 if (ConfigurationKey.isAttributeKey(getName()))
659                 {
660                     updateAttribute();
661                 }
662                 else
663                 {
664                     org.w3c.dom.Node parentElem = element.getParentNode();
665                     if (parentElem != null)
666                     {
667                         parentElem.removeChild(element);
668                     }
669                 }
670             }
671         }
672 
673         /***
674          * Updates the node's value if it represents an element node.
675          *
676          * @param value the new value
677          */
678         private void updateElement(Object value)
679         {
680             Text txtNode = findTextNodeForUpdate();
681             if (value == null)
682             {
683                 // remove text
684                 if (txtNode != null)
685                 {
686                     ((Element) getReference()).removeChild(txtNode);
687                 }
688             }
689             else
690             {
691                 if (txtNode == null)
692                 {
693                     txtNode = document
694                             .createTextNode(PropertyConverter.escapeDelimiters(
695                                     value.toString(), getDelimiter()));
696                     if (((Element) getReference()).getFirstChild() != null)
697                     {
698                         ((Element) getReference()).insertBefore(txtNode,
699                                 ((Element) getReference()).getFirstChild());
700                     }
701                     else
702                     {
703                         ((Element) getReference()).appendChild(txtNode);
704                     }
705                 }
706                 else
707                 {
708                     txtNode.setNodeValue(PropertyConverter.escapeDelimiters(
709                             value.toString(), getDelimiter()));
710                 }
711             }
712         }
713 
714         /***
715          * Updates the node's value if it represents an attribute.
716          *
717          */
718         private void updateAttribute()
719         {
720             XMLBuilderVisitor.updateAttribute(getParent(), getName());
721         }
722 
723         /***
724          * Returns the only text node of this element for update. This method is
725          * called when the element's text changes. Then all text nodes except
726          * for the first are removed. A reference to the first is returned or
727          * <b>null </b> if there is no text node at all.
728          *
729          * @return the first and only text node
730          */
731         private Text findTextNodeForUpdate()
732         {
733             Text result = null;
734             Element elem = (Element) getReference();
735             // Find all Text nodes
736             NodeList children = elem.getChildNodes();
737             Collection textNodes = new ArrayList();
738             for (int i = 0; i < children.getLength(); i++)
739             {
740                 org.w3c.dom.Node nd = children.item(i);
741                 if (nd instanceof Text)
742                 {
743                     if (result == null)
744                     {
745                         result = (Text) nd;
746                     }
747                     else
748                     {
749                         textNodes.add(nd);
750                     }
751                 }
752             }
753 
754             // We don't want CDATAs
755             if (result instanceof CDATASection)
756             {
757                 textNodes.add(result);
758                 result = null;
759             }
760 
761             // Remove all but the first Text node
762             for (Iterator it = textNodes.iterator(); it.hasNext();)
763             {
764                 elem.removeChild((org.w3c.dom.Node) it.next());
765             }
766             return result;
767         }
768     }
769 
770     /***
771      * A concrete <code>BuilderVisitor</code> that can construct XML
772      * documents.
773      */
774     static class XMLBuilderVisitor extends BuilderVisitor
775     {
776         /*** Stores the document to be constructed. */
777         private Document document;
778 
779         /***
780          * Creates a new instance of <code>XMLBuilderVisitor</code>
781          *
782          * @param doc the document to be created
783          */
784         public XMLBuilderVisitor(Document doc)
785         {
786             document = doc;
787         }
788 
789         /***
790          * Processes the node hierarchy and adds new nodes to the document.
791          *
792          * @param rootNode the root node
793          */
794         public void processDocument(Node rootNode)
795         {
796             rootNode.visit(this, null);
797         }
798 
799         /***
800          * Inserts a new node. This implementation ensures that the correct
801          * XML element is created and inserted between the given siblings.
802          *
803          * @param newNode the node to insert
804          * @param parent the parent node
805          * @param sibling1 the first sibling
806          * @param sibling2 the second sibling
807          * @return the new node
808          */
809         protected Object insert(Node newNode, Node parent, Node sibling1, Node sibling2)
810         {
811             if (ConfigurationKey.isAttributeKey(newNode.getName()))
812             {
813                 updateAttribute(parent, getElement(parent), newNode.getName());
814                 return null;
815             }
816 
817             else
818             {
819                 Element elem = document.createElement(newNode.getName());
820                 if (newNode.getValue() != null)
821                 {
822                     elem.appendChild(document.createTextNode(
823                             PropertyConverter.escapeDelimiters(newNode.getValue().toString(), getDelimiter())));
824                 }
825                 if (sibling2 == null)
826                 {
827                     getElement(parent).appendChild(elem);
828                 }
829                 else if (sibling1 != null)
830                 {
831                     getElement(parent).insertBefore(elem, getElement(sibling1).getNextSibling());
832                 }
833                 else
834                 {
835                     getElement(parent).insertBefore(elem, getElement(parent).getFirstChild());
836                 }
837                 return elem;
838             }
839         }
840 
841         /***
842          * Helper method for updating the value of the specified node's
843          * attribute with the given name.
844          *
845          * @param node the affected node
846          * @param elem the element that is associated with this node
847          * @param name the name of the affected attribute
848          */
849         private static void updateAttribute(Node node, Element elem, String name)
850         {
851             if (node != null && elem != null)
852             {
853                 List attrs = node.getChildren(name);
854                 StringBuffer buf = new StringBuffer();
855                 for (Iterator it = attrs.iterator(); it.hasNext();)
856                 {
857                     Node attr = (Node) it.next();
858                     if (attr.getValue() != null)
859                     {
860                         if (buf.length() > 0)
861                         {
862                             buf.append(getDelimiter());
863                         }
864                         buf.append(PropertyConverter.escapeDelimiters(attr
865                                 .getValue().toString(), getDelimiter()));
866                     }
867                     attr.setReference(elem);
868                 }
869 
870                 if (buf.length() < 1)
871                 {
872                     elem.removeAttribute(ConfigurationKey
873                             .removeAttributeMarkers(name));
874                 }
875                 else
876                 {
877                     elem.setAttribute(ConfigurationKey
878                             .removeAttributeMarkers(name), buf.toString());
879                 }
880             }
881         }
882 
883         /***
884          * Updates the value of the specified attribute of the given node.
885          * Because there can be multiple child nodes representing this attribute
886          * the new value is determined by iterating over all those child nodes.
887          *
888          * @param node the affected node
889          * @param name the name of the attribute
890          */
891         static void updateAttribute(Node node, String name)
892         {
893             if (node != null)
894             {
895                 updateAttribute(node, (Element) node.getReference(), name);
896             }
897         }
898 
899         /***
900          * Helper method for accessing the element of the specified node.
901          *
902          * @param node the node
903          * @return the element of this node
904          */
905         private Element getElement(Node node)
906         {
907             // special treatement for root node of the hierarchy
908             return (node.getName() != null) ? (Element) node.getReference() : document.getDocumentElement();
909         }
910     }
911 
912     /***
913      * A special implementation of the <code>FileConfiguration</code> interface that is
914      * used internally to implement the <code>FileConfiguration</code> methods
915      * for <code>XMLConfiguration</code>, too.
916      */
917     private class XMLFileConfigurationDelegate extends FileConfigurationDelegate
918     {
919         public void load(InputStream in) throws ConfigurationException
920         {
921             XMLConfiguration.this.load(in);
922         }
923     }
924 }