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 }