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