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