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