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