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