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