View Javadoc

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