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.configuration2; 019 020import javax.xml.parsers.DocumentBuilder; 021import javax.xml.parsers.DocumentBuilderFactory; 022import javax.xml.parsers.ParserConfigurationException; 023import javax.xml.transform.OutputKeys; 024import javax.xml.transform.Result; 025import javax.xml.transform.Source; 026import javax.xml.transform.Transformer; 027import javax.xml.transform.dom.DOMSource; 028import javax.xml.transform.stream.StreamResult; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.Reader; 032import java.io.StringReader; 033import java.io.StringWriter; 034import java.io.Writer; 035import java.net.URL; 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.Collections; 039import java.util.HashMap; 040import java.util.Iterator; 041import java.util.Map; 042 043import org.apache.commons.configuration2.convert.ListDelimiterHandler; 044import org.apache.commons.configuration2.ex.ConfigurationException; 045import org.apache.commons.configuration2.io.ConfigurationLogger; 046import org.apache.commons.configuration2.io.FileLocator; 047import org.apache.commons.configuration2.io.FileLocatorAware; 048import org.apache.commons.configuration2.io.InputStreamSupport; 049import org.apache.commons.configuration2.resolver.DefaultEntityResolver; 050import org.apache.commons.configuration2.tree.ImmutableNode; 051import org.apache.commons.configuration2.tree.NodeTreeWalker; 052import org.apache.commons.configuration2.tree.ReferenceNodeHandler; 053import org.apache.commons.lang3.StringUtils; 054import org.apache.commons.lang3.mutable.MutableObject; 055import org.w3c.dom.Attr; 056import org.w3c.dom.CDATASection; 057import org.w3c.dom.Document; 058import org.w3c.dom.Element; 059import org.w3c.dom.NamedNodeMap; 060import org.w3c.dom.Node; 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> 071 * A specialized hierarchical configuration class that is able to parse XML 072 * documents. 073 * </p> 074 * <p> 075 * The parsed document will be stored keeping its structure. The class also 076 * tries to preserve as much information from the loaded XML document as 077 * possible, including comments and processing instructions. These will be 078 * contained in documents created by the {@code save()} methods, too. 079 * </p> 080 * <p> 081 * Like other file based configuration classes this class maintains the name and 082 * path to the loaded configuration file. These properties can be altered using 083 * several setter methods, but they are not modified by {@code save()} and 084 * {@code load()} methods. If XML documents contain relative paths to other 085 * documents (e.g. to a DTD), these references are resolved based on the path 086 * set for this configuration. 087 * </p> 088 * <p> 089 * By inheriting from {@link AbstractConfiguration} this class provides some 090 * extended functionality, e.g. interpolation of property values. Like in 091 * {@link PropertiesConfiguration} property values can contain delimiter 092 * characters (the comma ',' per default) and are then split into multiple 093 * values. This works for XML attributes and text content of elements as well. 094 * The delimiter can be escaped by a backslash. As an example consider the 095 * following XML fragment: 096 * </p> 097 * 098 * <pre> 099 * <config> 100 * <array>10,20,30,40</array> 101 * <scalar>3\,1415</scalar> 102 * <cite text="To be or not to be\, this is the question!"/> 103 * </config> 104 * </pre> 105 * 106 * <p> 107 * Here the content of the {@code array} element will be split at the commas, so 108 * the {@code array} key will be assigned 4 values. In the {@code scalar} 109 * property and the {@code text} attribute of the {@code cite} element the comma 110 * is escaped, so that no splitting is performed. 111 * </p> 112 * <p> 113 * The configuration API allows setting multiple values for a single attribute, 114 * e.g. something like the following is legal (assuming that the default 115 * expression engine is used): 116 * </p> 117 * 118 * <pre> 119 * XMLConfiguration config = new XMLConfiguration(); 120 * config.addProperty("test.dir[@name]", "C:\\Temp\\"); 121 * config.addProperty("test.dir[@name]", "D:\\Data\\"); 122 * </pre> 123 * 124 * <p> 125 * However, in XML such a constellation is not supported; an attribute can 126 * appear only once for a single element. Therefore, an attempt to save a 127 * configuration which violates this condition will throw an exception. 128 * </p> 129 * <p> 130 * Like other {@code Configuration} implementations, {@code XMLConfiguration} 131 * uses a {@link ListDelimiterHandler} object for controlling list split 132 * operations. Per default, a list delimiter handler object is set which 133 * disables this feature. XML has a built-in support for complex structures 134 * including list properties; therefore, list splitting is not that relevant for 135 * this configuration type. Nevertheless, by setting an alternative 136 * {@code ListDelimiterHandler} implementation, this feature can be enabled. It 137 * works as for any other concrete {@code Configuration} implementation. 138 * </p> 139 * <p> 140 * Whitespace in the content of XML documents is trimmed per default. In most 141 * cases this is desired. However, sometimes whitespace is indeed important and 142 * should be treated as part of the value of a property as in the following 143 * example: 144 * </p> 145 * <pre> 146 * <indent> </indent> 147 * </pre> 148 * 149 * <p> 150 * Per default the spaces in the {@code indent} element will be trimmed 151 * resulting in an empty element. To tell {@code XMLConfiguration} that spaces 152 * are relevant the {@code xml:space} attribute can be used, which is defined in 153 * the <a href="http://www.w3.org/TR/REC-xml/#sec-white-space">XML 154 * specification</a>. This will look as follows: 155 * </p> 156 * <pre> 157 * <indent <strong>xml:space="preserve"</strong>> </indent> 158 * </pre> 159 * 160 * <p> 161 * The value of the {@code indent} property will now contain the spaces. 162 * </p> 163 * <p> 164 * {@code XMLConfiguration} implements the {@link FileBasedConfiguration} 165 * interface and thus can be used together with a file-based builder to load XML 166 * configuration files from various sources like files, URLs, or streams. 167 * </p> 168 * <p> 169 * Like other {@code Configuration} implementations, this class uses a 170 * {@code Synchronizer} object to control concurrent access. By choosing a 171 * suitable implementation of the {@code Synchronizer} interface, an instance 172 * can be made thread-safe or not. Note that access to most of the properties 173 * typically set through a builder is not protected by the {@code Synchronizer}. 174 * The intended usage is that these properties are set once at construction time 175 * through the builder and after that remain constant. If you wish to change 176 * such properties during life time of an instance, you have to use the 177 * {@code lock()} and {@code unlock()} methods manually to ensure that other 178 * threads see your changes. 179 * </p> 180 * <p> 181 * More information about the basic functionality supported by 182 * {@code XMLConfiguration} can be found at the user's guide at 183 * <a href="https://commons.apache.org/proper/commons-configuration/userguide/howto_basicfeatures.html"> 184 * Basic features and AbstractConfiguration</a>. There is 185 * also a separate chapter dealing with 186 * <a href="commons.apache.org/proper/commons-configuration/userguide/howto_xml.html"> 187 * XML Configurations</a> in special. 188 * </p> 189 * 190 * @since commons-configuration 1.0 191 */ 192public class XMLConfiguration extends BaseHierarchicalConfiguration implements 193 FileBasedConfiguration, FileLocatorAware, InputStreamSupport 194{ 195 /** Constant for the default indent size. */ 196 static final int DEFAULT_INDENT_SIZE = 2; 197 198 /** Constant for output property name used on a transformer to specify the indent amount. */ 199 static final String INDENT_AMOUNT_PROPERTY = "{http://xml.apache.org/xslt}indent-amount"; 200 201 /** Constant for the default root element name. */ 202 private static final String DEFAULT_ROOT_NAME = "configuration"; 203 204 /** Constant for the name of the space attribute.*/ 205 private static final String ATTR_SPACE = "xml:space"; 206 207 /** Constant for an internally used space attribute. */ 208 private static final String ATTR_SPACE_INTERNAL = "config-xml:space"; 209 210 /** Constant for the xml:space value for preserving whitespace.*/ 211 private static final String VALUE_PRESERVE = "preserve"; 212 213 /** Schema Langauge key for the parser */ 214 private static final String JAXP_SCHEMA_LANGUAGE = 215 "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; 216 217 /** Schema Language for the parser */ 218 private static final String W3C_XML_SCHEMA = 219 "http://www.w3.org/2001/XMLSchema"; 220 221 /** Stores the name of the root element. */ 222 private String rootElementName; 223 224 /** Stores the public ID from the DOCTYPE.*/ 225 private String publicID; 226 227 /** Stores the system ID from the DOCTYPE.*/ 228 private String systemID; 229 230 /** Stores the document builder that should be used for loading.*/ 231 private DocumentBuilder documentBuilder; 232 233 /** Stores a flag whether DTD or Schema validation should be performed.*/ 234 private boolean validating; 235 236 /** Stores a flag whether DTD or Schema validation is used */ 237 private boolean schemaValidation; 238 239 /** The EntityResolver to use */ 240 private EntityResolver entityResolver = new DefaultEntityResolver(); 241 242 /** The current file locator. */ 243 private FileLocator locator; 244 245 /** 246 * Creates a new instance of {@code XMLConfiguration}. 247 */ 248 public XMLConfiguration() 249 { 250 super(); 251 initLogger(new ConfigurationLogger(XMLConfiguration.class)); 252 } 253 254 /** 255 * Creates a new instance of {@code XMLConfiguration} and copies the 256 * content of the passed in configuration into this object. Note that only 257 * the data of the passed in configuration will be copied. If, for instance, 258 * the other configuration is a {@code XMLConfiguration}, too, 259 * things like comments or processing instructions will be lost. 260 * 261 * @param c the configuration to copy 262 * @since 1.4 263 */ 264 public XMLConfiguration(final HierarchicalConfiguration<ImmutableNode> c) 265 { 266 super(c); 267 rootElementName = 268 c != null ? c.getRootElementName() : null; 269 initLogger(new ConfigurationLogger(XMLConfiguration.class)); 270 } 271 272 /** 273 * Returns the name of the root element. If this configuration was loaded 274 * from a XML document, the name of this document's root element is 275 * returned. Otherwise it is possible to set a name for the root element 276 * that will be used when this configuration is stored. 277 * 278 * @return the name of the root element 279 */ 280 @Override 281 protected String getRootElementNameInternal() 282 { 283 final Document doc = getDocument(); 284 if (doc == null) 285 { 286 return rootElementName == null ? DEFAULT_ROOT_NAME : rootElementName; 287 } 288 return doc.getDocumentElement().getNodeName(); 289 } 290 291 /** 292 * Sets the name of the root element. This name is used when this 293 * configuration object is stored in an XML file. Note that setting the name 294 * of the root element works only if this configuration has been newly 295 * created. If the configuration was loaded from an XML file, the name 296 * cannot be changed and an {@code UnsupportedOperationException} 297 * exception is thrown. Whether this configuration has been loaded from an 298 * XML document or not can be found out using the {@code getDocument()} 299 * method. 300 * 301 * @param name the name of the root element 302 */ 303 public void setRootElementName(final String name) 304 { 305 beginRead(true); 306 try 307 { 308 if (getDocument() != null) 309 { 310 throw new UnsupportedOperationException( 311 "The name of the root element " 312 + "cannot be changed when loaded from an XML document!"); 313 } 314 rootElementName = name; 315 } 316 finally 317 { 318 endRead(); 319 } 320 } 321 322 /** 323 * Returns the {@code DocumentBuilder} object that is used for 324 * loading documents. If no specific builder has been set, this method 325 * returns <b>null</b>. 326 * 327 * @return the {@code DocumentBuilder} for loading new documents 328 * @since 1.2 329 */ 330 public DocumentBuilder getDocumentBuilder() 331 { 332 return documentBuilder; 333 } 334 335 /** 336 * Sets the {@code DocumentBuilder} object to be used for loading 337 * documents. This method makes it possible to specify the exact document 338 * builder. So an application can create a builder, configure it for its 339 * special needs, and then pass it to this method. 340 * 341 * @param documentBuilder the document builder to be used; if undefined, a 342 * default builder will be used 343 * @since 1.2 344 */ 345 public void setDocumentBuilder(final DocumentBuilder documentBuilder) 346 { 347 this.documentBuilder = documentBuilder; 348 } 349 350 /** 351 * Returns the public ID of the DOCTYPE declaration from the loaded XML 352 * document. This is <b>null</b> if no document has been loaded yet or if 353 * the document does not contain a DOCTYPE declaration with a public ID. 354 * 355 * @return the public ID 356 * @since 1.3 357 */ 358 public String getPublicID() 359 { 360 beginRead(false); 361 try 362 { 363 return publicID; 364 } 365 finally 366 { 367 endRead(); 368 } 369 } 370 371 /** 372 * Sets the public ID of the DOCTYPE declaration. When this configuration is 373 * saved, a DOCTYPE declaration will be constructed that contains this 374 * public ID. 375 * 376 * @param publicID the public ID 377 * @since 1.3 378 */ 379 public void setPublicID(final String publicID) 380 { 381 beginWrite(false); 382 try 383 { 384 this.publicID = publicID; 385 } 386 finally 387 { 388 endWrite(); 389 } 390 } 391 392 /** 393 * Returns the system ID of the DOCTYPE declaration from the loaded XML 394 * document. This is <b>null</b> if no document has been loaded yet or if 395 * the document does not contain a DOCTYPE declaration with a system ID. 396 * 397 * @return the system ID 398 * @since 1.3 399 */ 400 public String getSystemID() 401 { 402 beginRead(false); 403 try 404 { 405 return systemID; 406 } 407 finally 408 { 409 endRead(); 410 } 411 } 412 413 /** 414 * Sets the system ID of the DOCTYPE declaration. When this configuration is 415 * saved, a DOCTYPE declaration will be constructed that contains this 416 * system ID. 417 * 418 * @param systemID the system ID 419 * @since 1.3 420 */ 421 public void setSystemID(final String systemID) 422 { 423 beginWrite(false); 424 try 425 { 426 this.systemID = systemID; 427 } 428 finally 429 { 430 endWrite(); 431 } 432 } 433 434 /** 435 * Returns the value of the validating flag. 436 * 437 * @return the validating flag 438 * @since 1.2 439 */ 440 public boolean isValidating() 441 { 442 return validating; 443 } 444 445 /** 446 * Sets the value of the validating flag. This flag determines whether 447 * DTD/Schema validation should be performed when loading XML documents. This 448 * flag is evaluated only if no custom {@code DocumentBuilder} was set. 449 * 450 * @param validating the validating flag 451 * @since 1.2 452 */ 453 public void setValidating(final boolean validating) 454 { 455 if (!schemaValidation) 456 { 457 this.validating = validating; 458 } 459 } 460 461 462 /** 463 * Returns the value of the schemaValidation flag. 464 * 465 * @return the schemaValidation flag 466 * @since 1.7 467 */ 468 public boolean isSchemaValidation() 469 { 470 return schemaValidation; 471 } 472 473 /** 474 * Sets the value of the schemaValidation flag. This flag determines whether 475 * DTD or Schema validation should be used. This 476 * flag is evaluated only if no custom {@code DocumentBuilder} was set. 477 * If set to true the XML document must contain a schemaLocation definition 478 * that provides resolvable hints to the required schemas. 479 * 480 * @param schemaValidation the validating flag 481 * @since 1.7 482 */ 483 public void setSchemaValidation(final boolean schemaValidation) 484 { 485 this.schemaValidation = schemaValidation; 486 if (schemaValidation) 487 { 488 this.validating = true; 489 } 490 } 491 492 /** 493 * Sets a new EntityResolver. Setting this will cause RegisterEntityId to have no 494 * effect. 495 * @param resolver The EntityResolver to use. 496 * @since 1.7 497 */ 498 public void setEntityResolver(final EntityResolver resolver) 499 { 500 this.entityResolver = resolver; 501 } 502 503 /** 504 * Returns the EntityResolver. 505 * @return The EntityResolver. 506 * @since 1.7 507 */ 508 public EntityResolver getEntityResolver() 509 { 510 return this.entityResolver; 511 } 512 513 /** 514 * Returns the XML document this configuration was loaded from. The return 515 * value is <b>null</b> if this configuration was not loaded from a XML 516 * document. 517 * 518 * @return the XML document this configuration was loaded from 519 */ 520 public Document getDocument() 521 { 522 final XMLDocumentHelper docHelper = getDocumentHelper(); 523 return docHelper != null ? docHelper.getDocument() : null; 524 } 525 526 /** 527 * Returns the helper object for managing the underlying document. 528 * 529 * @return the {@code XMLDocumentHelper} 530 */ 531 private XMLDocumentHelper getDocumentHelper() 532 { 533 final ReferenceNodeHandler handler = getReferenceHandler(); 534 return (XMLDocumentHelper) handler.getReference(handler.getRootNode()); 535 } 536 537 /** 538 * Returns the extended node handler with support for references. 539 * 540 * @return the {@code ReferenceNodeHandler} 541 */ 542 private ReferenceNodeHandler getReferenceHandler() 543 { 544 return getSubConfigurationParentModel().getReferenceNodeHandler(); 545 } 546 547 /** 548 * Initializes this configuration from an XML document. 549 * 550 * @param docHelper the helper object with the document to be parsed 551 * @param elemRefs a flag whether references to the XML elements should be set 552 */ 553 private void initProperties(final XMLDocumentHelper docHelper, final boolean elemRefs) 554 { 555 final Document document = docHelper.getDocument(); 556 setPublicID(docHelper.getSourcePublicID()); 557 setSystemID(docHelper.getSourceSystemID()); 558 559 final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(); 560 final MutableObject<String> rootValue = new MutableObject<>(); 561 final Map<ImmutableNode, Object> elemRefMap = 562 elemRefs ? new HashMap<>() : null; 563 final Map<String, String> attributes = 564 constructHierarchy(rootBuilder, rootValue, 565 document.getDocumentElement(), elemRefMap, true, 0); 566 attributes.remove(ATTR_SPACE_INTERNAL); 567 final ImmutableNode top = 568 rootBuilder.value(rootValue.getValue()) 569 .addAttributes(attributes).create(); 570 getSubConfigurationParentModel().mergeRoot(top, 571 document.getDocumentElement().getTagName(), elemRefMap, 572 elemRefs ? docHelper : null, this); 573 } 574 575 /** 576 * Helper method for building the internal storage hierarchy. The XML 577 * elements are transformed into node objects. 578 * 579 * @param node a builder for the current node 580 * @param refValue stores the text value of the element 581 * @param element the current XML element 582 * @param elemRefs a map for assigning references objects to nodes; can be 583 * <b>null</b>, then reference objects are irrelevant 584 * @param trim a flag whether the text content of elements should be 585 * trimmed; this controls the whitespace handling 586 * @param level the current level in the hierarchy 587 * @return a map with all attribute values extracted for the current node; 588 * this map also contains the value of the trim flag for this node 589 * under the key {@value #ATTR_SPACE} 590 */ 591 private Map<String, String> constructHierarchy(final ImmutableNode.Builder node, 592 final MutableObject<String> refValue, final Element element, 593 final Map<ImmutableNode, Object> elemRefs, final boolean trim, final int level) 594 { 595 final boolean trimFlag = shouldTrim(element, trim); 596 final Map<String, String> attributes = processAttributes(element); 597 attributes.put(ATTR_SPACE_INTERNAL, String.valueOf(trimFlag)); 598 final StringBuilder buffer = new StringBuilder(); 599 final NodeList list = element.getChildNodes(); 600 boolean hasChildren = false; 601 602 for (int i = 0; i < list.getLength(); i++) 603 { 604 final org.w3c.dom.Node w3cNode = list.item(i); 605 if (w3cNode instanceof Element) 606 { 607 final Element child = (Element) w3cNode; 608 final ImmutableNode.Builder childNode = new ImmutableNode.Builder(); 609 childNode.name(child.getTagName()); 610 final MutableObject<String> refChildValue = 611 new MutableObject<>(); 612 final Map<String, String> attrmap = 613 constructHierarchy(childNode, refChildValue, child, 614 elemRefs, trimFlag, level + 1); 615 final Boolean childTrim = Boolean.valueOf(attrmap.remove(ATTR_SPACE_INTERNAL)); 616 childNode.addAttributes(attrmap); 617 final ImmutableNode newChild = 618 createChildNodeWithValue(node, childNode, child, 619 refChildValue.getValue(), 620 childTrim.booleanValue(), attrmap, elemRefs); 621 if (elemRefs != null && !elemRefs.containsKey(newChild)) 622 { 623 elemRefs.put(newChild, child); 624 } 625 hasChildren = true; 626 } 627 else if (w3cNode instanceof Text) 628 { 629 final Text data = (Text) w3cNode; 630 buffer.append(data.getData()); 631 } 632 } 633 634 boolean childrenFlag = false; 635 if (hasChildren || trimFlag) 636 { 637 childrenFlag = hasChildren || attributes.size() > 1; 638 } 639 final String text = determineValue(buffer.toString(), childrenFlag, trimFlag); 640 if (text.length() > 0 || (!childrenFlag && level != 0)) 641 { 642 refValue.setValue(text); 643 } 644 return attributes; 645 } 646 647 /** 648 * Determines the value of a configuration node. This method mainly checks 649 * whether the text value is to be trimmed or not. This is normally defined 650 * by the trim flag. However, if the node has children and its content is 651 * only whitespace, then it makes no sense to store any value; this would 652 * only scramble layout when the configuration is saved again. 653 * 654 * @param content the text content of this node 655 * @param hasChildren a flag whether the node has children 656 * @param trimFlag the trim flag 657 * @return the value to be stored for this node 658 */ 659 private static String determineValue(final String content, final boolean hasChildren, 660 final boolean trimFlag) 661 { 662 final boolean shouldTrim = 663 trimFlag || (StringUtils.isBlank(content) && hasChildren); 664 return shouldTrim ? content.trim() : content; 665 } 666 667 /** 668 * Helper method for initializing the attributes of a configuration node 669 * from the given XML element. 670 * 671 * @param element the current XML element 672 * @return a map with all attribute values extracted for the current node 673 */ 674 private static Map<String, String> processAttributes(final Element element) 675 { 676 final NamedNodeMap attributes = element.getAttributes(); 677 final Map<String, String> attrmap = new HashMap<>(); 678 679 for (int i = 0; i < attributes.getLength(); ++i) 680 { 681 final org.w3c.dom.Node w3cNode = attributes.item(i); 682 if (w3cNode instanceof Attr) 683 { 684 final Attr attr = (Attr) w3cNode; 685 attrmap.put(attr.getName(), attr.getValue()); 686 } 687 } 688 689 return attrmap; 690 } 691 692 /** 693 * Creates a new child node, assigns its value, and adds it to its parent. 694 * This method also deals with elements whose value is a list. In this case 695 * multiple child elements must be added. The return value is the first 696 * child node which was added. 697 * 698 * @param parent the builder for the parent element 699 * @param child the builder for the child element 700 * @param elem the associated XML element 701 * @param value the value of the child element 702 * @param trim flag whether texts of elements should be trimmed 703 * @param attrmap a map with the attributes of the current node 704 * @param elemRefs a map for assigning references objects to nodes; can be 705 * <b>null</b>, then reference objects are irrelevant 706 * @return the first child node added to the parent 707 */ 708 private ImmutableNode createChildNodeWithValue(final ImmutableNode.Builder parent, 709 final ImmutableNode.Builder child, final Element elem, final String value, 710 final boolean trim, final Map<String, String> attrmap, 711 final Map<ImmutableNode, Object> elemRefs) 712 { 713 ImmutableNode addedChildNode; 714 Collection<String> values; 715 716 if (value != null) 717 { 718 values = getListDelimiterHandler().split(value, trim); 719 } 720 else 721 { 722 values = Collections.emptyList(); 723 } 724 725 if (values.size() > 1) 726 { 727 final Map<ImmutableNode, Object> refs = isSingleElementList(elem) ? elemRefs : null; 728 final Iterator<String> it = values.iterator(); 729 // Create new node for the original child's first value 730 child.value(it.next()); 731 addedChildNode = child.create(); 732 parent.addChild(addedChildNode); 733 XMLListReference.assignListReference(refs, addedChildNode, elem); 734 735 // add multiple new children 736 while (it.hasNext()) 737 { 738 final ImmutableNode.Builder c = new ImmutableNode.Builder(); 739 c.name(addedChildNode.getNodeName()); 740 c.value(it.next()); 741 c.addAttributes(attrmap); 742 final ImmutableNode newChild = c.create(); 743 parent.addChild(newChild); 744 XMLListReference.assignListReference(refs, newChild, null); 745 } 746 } 747 else if (values.size() == 1) 748 { 749 // we will have to replace the value because it might 750 // contain escaped delimiters 751 child.value(values.iterator().next()); 752 addedChildNode = child.create(); 753 parent.addChild(addedChildNode); 754 } 755 else 756 { 757 addedChildNode = child.create(); 758 parent.addChild(addedChildNode); 759 } 760 761 return addedChildNode; 762 } 763 764 /** 765 * Checks whether an element defines a complete list. If this is the case, 766 * extended list handling can be applied. 767 * 768 * @param element the element to be checked 769 * @return a flag whether this is the only element defining the list 770 */ 771 private static boolean isSingleElementList(final Element element) 772 { 773 final Node parentNode = element.getParentNode(); 774 return countChildElements(parentNode, element.getTagName()) == 1; 775 } 776 777 /** 778 * Determines the number of child elements of this given node with the 779 * specified node name. 780 * 781 * @param parent the parent node 782 * @param name the name in question 783 * @return the number of child elements with this name 784 */ 785 private static int countChildElements(final Node parent, final String name) 786 { 787 final NodeList childNodes = parent.getChildNodes(); 788 int count = 0; 789 for (int i = 0; i < childNodes.getLength(); i++) 790 { 791 final Node item = childNodes.item(i); 792 if (item instanceof Element) 793 { 794 if (name.equals(((Element) item).getTagName())) 795 { 796 count++; 797 } 798 } 799 } 800 return count; 801 } 802 803 /** 804 * Checks whether the content of the current XML element should be trimmed. 805 * This method checks whether a {@code xml:space} attribute is 806 * present and evaluates its value. See <a 807 * href="http://www.w3.org/TR/REC-xml/#sec-white-space"> 808 * http://www.w3.org/TR/REC-xml/#sec-white-space</a> for more details. 809 * 810 * @param element the current XML element 811 * @param currentTrim the current trim flag 812 * @return a flag whether the content of this element should be trimmed 813 */ 814 private static boolean shouldTrim(final Element element, final boolean currentTrim) 815 { 816 final Attr attr = element.getAttributeNode(ATTR_SPACE); 817 818 if (attr == null) 819 { 820 return currentTrim; 821 } 822 return !VALUE_PRESERVE.equals(attr.getValue()); 823 } 824 825 /** 826 * Creates the {@code DocumentBuilder} to be used for loading files. 827 * This implementation checks whether a specific 828 * {@code DocumentBuilder} has been set. If this is the case, this 829 * one is used. Otherwise a default builder is created. Depending on the 830 * value of the validating flag this builder will be a validating or a non 831 * validating {@code DocumentBuilder}. 832 * 833 * @return the {@code DocumentBuilder} for loading configuration 834 * files 835 * @throws ParserConfigurationException if an error occurs 836 * @since 1.2 837 */ 838 protected DocumentBuilder createDocumentBuilder() 839 throws ParserConfigurationException 840 { 841 if (getDocumentBuilder() != null) 842 { 843 return getDocumentBuilder(); 844 } 845 final DocumentBuilderFactory factory = DocumentBuilderFactory 846 .newInstance(); 847 if (isValidating()) 848 { 849 factory.setValidating(true); 850 if (isSchemaValidation()) 851 { 852 factory.setNamespaceAware(true); 853 factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); 854 } 855 } 856 857 final DocumentBuilder result = factory.newDocumentBuilder(); 858 result.setEntityResolver(this.entityResolver); 859 860 if (isValidating()) 861 { 862 // register an error handler which detects validation errors 863 result.setErrorHandler(new DefaultHandler() 864 { 865 @Override 866 public void error(final SAXParseException ex) throws SAXException 867 { 868 throw ex; 869 } 870 }); 871 } 872 return result; 873 } 874 875 /** 876 * Creates and initializes the transformer used for save operations. This 877 * base implementation initializes all of the default settings like 878 * indentation mode and the DOCTYPE. Derived classes may overload this method 879 * if they have specific needs. 880 * 881 * @return the transformer to use for a save operation 882 * @throws ConfigurationException if an error occurs 883 * @since 1.3 884 */ 885 protected Transformer createTransformer() throws ConfigurationException 886 { 887 final Transformer transformer = XMLDocumentHelper.createTransformer(); 888 889 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 890 transformer.setOutputProperty(INDENT_AMOUNT_PROPERTY, Integer.toString(DEFAULT_INDENT_SIZE)); 891 if (locator != null && locator.getEncoding() != null) 892 { 893 transformer.setOutputProperty(OutputKeys.ENCODING, locator.getEncoding()); 894 } 895 if (publicID != null) 896 { 897 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicID); 898 } 899 if (systemID != null) 900 { 901 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemID); 902 } 903 904 return transformer; 905 } 906 907 /** 908 * Creates a DOM document from the internal tree of configuration nodes. 909 * 910 * @return the new document 911 * @throws ConfigurationException if an error occurs 912 */ 913 private Document createDocument() throws ConfigurationException 914 { 915 final ReferenceNodeHandler handler = getReferenceHandler(); 916 final XMLDocumentHelper docHelper = 917 (XMLDocumentHelper) handler.getReference(handler.getRootNode()); 918 final XMLDocumentHelper newHelper = 919 docHelper == null ? XMLDocumentHelper 920 .forNewDocument(getRootElementName()) : docHelper 921 .createCopy(); 922 923 final XMLBuilderVisitor builder = 924 new XMLBuilderVisitor(newHelper, getListDelimiterHandler()); 925 builder.handleRemovedNodes(handler); 926 builder.processDocument(handler); 927 initRootElementText(newHelper.getDocument(), getModel() 928 .getNodeHandler().getRootNode().getValue()); 929 return newHelper.getDocument(); 930 } 931 932 /** 933 * Sets the text of the root element of a newly created XML Document. 934 * 935 * @param doc the document 936 * @param value the new text to be set 937 */ 938 private void initRootElementText(final Document doc, final Object value) 939 { 940 final Element elem = doc.getDocumentElement(); 941 final NodeList children = elem.getChildNodes(); 942 943 // Remove all existing text nodes 944 for (int i = 0; i < children.getLength(); i++) 945 { 946 final org.w3c.dom.Node nd = children.item(i); 947 if (nd.getNodeType() == org.w3c.dom.Node.TEXT_NODE) 948 { 949 elem.removeChild(nd); 950 } 951 } 952 953 if (value != null) 954 { 955 // Add a new text node 956 elem.appendChild(doc.createTextNode(String.valueOf(value))); 957 } 958 } 959 960 /** 961 * {@inheritDoc} Stores the passed in locator for the upcoming IO operation. 962 */ 963 @Override 964 public void initFileLocator(final FileLocator loc) 965 { 966 locator = loc; 967 } 968 969 /** 970 * Loads the configuration from the given reader. 971 * Note that the {@code clear()} method is not called, so 972 * the properties contained in the loaded file will be added to the 973 * current set of properties. 974 * 975 * @param in the reader 976 * @throws ConfigurationException if an error occurs 977 * @throws IOException if an IO error occurs 978 */ 979 @Override 980 public void read(final Reader in) throws ConfigurationException, IOException 981 { 982 load(new InputSource(in)); 983 } 984 985 /** 986 * Loads the configuration from the given input stream. This is analogous to 987 * {@link #read(Reader)}, but data is read from a stream. Note that this 988 * method will be called most time when reading an XML configuration source. 989 * By reading XML documents directly from an input stream, the file's 990 * encoding can be correctly dealt with. 991 * 992 * @param in the input stream 993 * @throws ConfigurationException if an error occurs 994 * @throws IOException if an IO error occurs 995 */ 996 @Override 997 public void read(final InputStream in) throws ConfigurationException, IOException 998 { 999 load(new InputSource(in)); 1000 } 1001 1002 /** 1003 * Loads a configuration file from the specified input source. 1004 * 1005 * @param source the input source 1006 * @throws ConfigurationException if an error occurs 1007 */ 1008 private void load(final InputSource source) throws ConfigurationException 1009 { 1010 if (locator == null) 1011 { 1012 throw new ConfigurationException("Load operation not properly " 1013 + "initialized! Do not call read(InputStream) directly," 1014 + " but use a FileHandler to load a configuration."); 1015 } 1016 1017 try 1018 { 1019 final URL sourceURL = locator.getSourceURL(); 1020 if (sourceURL != null) 1021 { 1022 source.setSystemId(sourceURL.toString()); 1023 } 1024 1025 final DocumentBuilder builder = createDocumentBuilder(); 1026 final Document newDocument = builder.parse(source); 1027 final Document oldDocument = getDocument(); 1028 initProperties(XMLDocumentHelper.forSourceDocument(newDocument), 1029 oldDocument == null); 1030 } 1031 catch (final SAXParseException spe) 1032 { 1033 throw new ConfigurationException("Error parsing " + source.getSystemId(), spe); 1034 } 1035 catch (final Exception e) 1036 { 1037 this.getLogger().debug("Unable to load the configuration: " + e); 1038 throw new ConfigurationException("Unable to load the configuration", e); 1039 } 1040 } 1041 1042 /** 1043 * Saves the configuration to the specified writer. 1044 * 1045 * @param writer the writer used to save the configuration 1046 * @throws ConfigurationException if an error occurs 1047 * @throws IOException if an IO error occurs 1048 */ 1049 @Override 1050 public void write(final Writer writer) throws ConfigurationException, IOException 1051 { 1052 write(writer, createTransformer()); 1053 } 1054 1055 /** 1056 * Saves the configuration to the specified writer. 1057 * 1058 * @param writer the writer used to save the configuration. 1059 * @param transformer How to transform this configuration. 1060 * @throws ConfigurationException if an error occurs. 1061 * @since 2.7.0 1062 */ 1063 public void write(final Writer writer, final Transformer transformer) throws ConfigurationException 1064 { 1065 final Source source = new DOMSource(createDocument()); 1066 final Result result = new StreamResult(writer); 1067 XMLDocumentHelper.transform(transformer, source, result); 1068 } 1069 1070 /** 1071 * Validate the document against the Schema. 1072 * @throws ConfigurationException if the validation fails. 1073 */ 1074 public void validate() throws ConfigurationException 1075 { 1076 beginWrite(false); 1077 try 1078 { 1079 final Transformer transformer = createTransformer(); 1080 final Source source = new DOMSource(createDocument()); 1081 final StringWriter writer = new StringWriter(); 1082 final Result result = new StreamResult(writer); 1083 XMLDocumentHelper.transform(transformer, source, result); 1084 final Reader reader = new StringReader(writer.getBuffer().toString()); 1085 final DocumentBuilder builder = createDocumentBuilder(); 1086 builder.parse(new InputSource(reader)); 1087 } 1088 catch (final SAXException e) 1089 { 1090 throw new ConfigurationException("Validation failed", e); 1091 } 1092 catch (final IOException e) 1093 { 1094 throw new ConfigurationException("Validation failed", e); 1095 } 1096 catch (final ParserConfigurationException pce) 1097 { 1098 throw new ConfigurationException("Validation failed", pce); 1099 } 1100 finally 1101 { 1102 endWrite(); 1103 } 1104 } 1105 1106 /** 1107 * A concrete {@code BuilderVisitor} that can construct XML 1108 * documents. 1109 */ 1110 static class XMLBuilderVisitor extends BuilderVisitor 1111 { 1112 /** Stores the document to be constructed. */ 1113 private final Document document; 1114 1115 /** The element mapping. */ 1116 private final Map<Node, Node> elementMapping; 1117 1118 /** A mapping for the references for new nodes. */ 1119 private final Map<ImmutableNode, Element> newElements; 1120 1121 /** Stores the list delimiter handler .*/ 1122 private final ListDelimiterHandler listDelimiterHandler; 1123 1124 /** 1125 * Creates a new instance of {@code XMLBuilderVisitor}. 1126 * 1127 * @param docHelper the document helper 1128 * @param handler the delimiter handler for properties with multiple 1129 * values 1130 */ 1131 public XMLBuilderVisitor(final XMLDocumentHelper docHelper, 1132 final ListDelimiterHandler handler) 1133 { 1134 document = docHelper.getDocument(); 1135 elementMapping = docHelper.getElementMapping(); 1136 listDelimiterHandler = handler; 1137 newElements = new HashMap<>(); 1138 } 1139 1140 /** 1141 * Processes the specified document, updates element values, and adds 1142 * new nodes to the hierarchy. 1143 * 1144 * @param refHandler the {@code ReferenceNodeHandler} 1145 */ 1146 public void processDocument(final ReferenceNodeHandler refHandler) 1147 { 1148 updateAttributes(refHandler.getRootNode(), document.getDocumentElement()); 1149 NodeTreeWalker.INSTANCE.walkDFS(refHandler.getRootNode(), this, 1150 refHandler); 1151 } 1152 1153 /** 1154 * Updates the current XML document regarding removed nodes. The 1155 * elements associated with removed nodes are removed from the document. 1156 * 1157 * @param refHandler the {@code ReferenceNodeHandler} 1158 */ 1159 public void handleRemovedNodes(final ReferenceNodeHandler refHandler) 1160 { 1161 for (final Object ref : refHandler.removedReferences()) 1162 { 1163 if (ref instanceof Node) 1164 { 1165 final Node removedElem = (Node) ref; 1166 removeReference((Element) elementMapping.get(removedElem)); 1167 } 1168 } 1169 } 1170 1171 /** 1172 * {@inheritDoc} This implementation ensures that the correct XML 1173 * element is created and inserted between the given siblings. 1174 */ 1175 @Override 1176 protected void insert(final ImmutableNode newNode, final ImmutableNode parent, 1177 final ImmutableNode sibling1, final ImmutableNode sibling2, 1178 final ReferenceNodeHandler refHandler) 1179 { 1180 if (XMLListReference.isListNode(newNode, refHandler)) 1181 { 1182 return; 1183 } 1184 1185 final Element elem = document.createElement(newNode.getNodeName()); 1186 newElements.put(newNode, elem); 1187 updateAttributes(newNode, elem); 1188 if (newNode.getValue() != null) 1189 { 1190 final String txt = 1191 String.valueOf(listDelimiterHandler.escape( 1192 newNode.getValue(), 1193 ListDelimiterHandler.NOOP_TRANSFORMER)); 1194 elem.appendChild(document.createTextNode(txt)); 1195 } 1196 if (sibling2 == null) 1197 { 1198 getElement(parent, refHandler).appendChild(elem); 1199 } 1200 else if (sibling1 != null) 1201 { 1202 getElement(parent, refHandler).insertBefore(elem, 1203 getElement(sibling1, refHandler).getNextSibling()); 1204 } 1205 else 1206 { 1207 getElement(parent, refHandler).insertBefore(elem, 1208 getElement(parent, refHandler).getFirstChild()); 1209 } 1210 } 1211 1212 /** 1213 * {@inheritDoc} This implementation determines the XML element 1214 * associated with the given node. Then this element's value and 1215 * attributes are set accordingly. 1216 */ 1217 @Override 1218 protected void update(final ImmutableNode node, final Object reference, 1219 final ReferenceNodeHandler refHandler) 1220 { 1221 if (XMLListReference.isListNode(node, refHandler)) 1222 { 1223 if (XMLListReference.isFirstListItem(node, refHandler)) 1224 { 1225 final String value = XMLListReference.listValue(node, refHandler, listDelimiterHandler); 1226 updateElement(node, refHandler, value); 1227 } 1228 } 1229 else 1230 { 1231 final Object value = listDelimiterHandler.escape(refHandler.getValue(node), 1232 ListDelimiterHandler.NOOP_TRANSFORMER); 1233 updateElement(node, refHandler, value); 1234 } 1235 } 1236 1237 private void updateElement(final ImmutableNode node, final ReferenceNodeHandler refHandler, 1238 final Object value) 1239 { 1240 final Element element = getElement(node, refHandler); 1241 updateElement(element, value); 1242 updateAttributes(node, element); 1243 } 1244 1245 /** 1246 * Updates the node's value if it represents an element node. 1247 * 1248 * @param element the element 1249 * @param value the new value 1250 */ 1251 private void updateElement(final Element element, final Object value) 1252 { 1253 Text txtNode = findTextNodeForUpdate(element); 1254 if (value == null) 1255 { 1256 // remove text 1257 if (txtNode != null) 1258 { 1259 element.removeChild(txtNode); 1260 } 1261 } 1262 else 1263 { 1264 final String newValue = String.valueOf(value); 1265 if (txtNode == null) 1266 { 1267 txtNode = document.createTextNode(newValue); 1268 if (element.getFirstChild() != null) 1269 { 1270 element.insertBefore(txtNode, element.getFirstChild()); 1271 } 1272 else 1273 { 1274 element.appendChild(txtNode); 1275 } 1276 } 1277 else 1278 { 1279 txtNode.setNodeValue(newValue); 1280 } 1281 } 1282 } 1283 1284 /** 1285 * Updates the associated XML elements when a node is removed. 1286 * @param element the element to be removed 1287 */ 1288 private void removeReference(final Element element) 1289 { 1290 final org.w3c.dom.Node parentElem = element.getParentNode(); 1291 if (parentElem != null) 1292 { 1293 parentElem.removeChild(element); 1294 } 1295 } 1296 1297 /** 1298 * Helper method for accessing the element of the specified node. 1299 * 1300 * @param node the node 1301 * @param refHandler the {@code ReferenceNodeHandler} 1302 * @return the element of this node 1303 */ 1304 private Element getElement(final ImmutableNode node, 1305 final ReferenceNodeHandler refHandler) 1306 { 1307 final Element elementNew = newElements.get(node); 1308 if (elementNew != null) 1309 { 1310 return elementNew; 1311 } 1312 1313 // special treatment for root node of the hierarchy 1314 final Object reference = refHandler.getReference(node); 1315 Node element; 1316 if (reference instanceof XMLDocumentHelper) 1317 { 1318 element = 1319 ((XMLDocumentHelper) reference).getDocument() 1320 .getDocumentElement(); 1321 } 1322 else if (reference instanceof XMLListReference) 1323 { 1324 element = ((XMLListReference) reference).getElement(); 1325 } 1326 else 1327 { 1328 element = (Node) reference; 1329 } 1330 return element != null ? (Element) elementMapping.get(element) 1331 : document.getDocumentElement(); 1332 } 1333 1334 /** 1335 * Helper method for updating the values of all attributes of the 1336 * specified node. 1337 * 1338 * @param node the affected node 1339 * @param elem the element that is associated with this node 1340 */ 1341 private static void updateAttributes(final ImmutableNode node, final Element elem) 1342 { 1343 if (node != null && elem != null) 1344 { 1345 clearAttributes(elem); 1346 for (final Map.Entry<String, Object> e : node.getAttributes() 1347 .entrySet()) 1348 { 1349 if (e.getValue() != null) 1350 { 1351 elem.setAttribute(e.getKey(), e.getValue().toString()); 1352 } 1353 } 1354 } 1355 } 1356 1357 /** 1358 * Removes all attributes of the given element. 1359 * 1360 * @param elem the element 1361 */ 1362 private static void clearAttributes(final Element elem) 1363 { 1364 final NamedNodeMap attributes = elem.getAttributes(); 1365 for (int i = 0; i < attributes.getLength(); i++) 1366 { 1367 elem.removeAttribute(attributes.item(i).getNodeName()); 1368 } 1369 } 1370 1371 /** 1372 * Returns the only text node of an element for update. This method is 1373 * called when the element's text changes. Then all text nodes except 1374 * for the first are removed. A reference to the first is returned or 1375 * <b>null</b> if there is no text node at all. 1376 * 1377 * @param elem the element 1378 * @return the first and only text node 1379 */ 1380 private static Text findTextNodeForUpdate(final Element elem) 1381 { 1382 Text result = null; 1383 // Find all Text nodes 1384 final NodeList children = elem.getChildNodes(); 1385 final Collection<org.w3c.dom.Node> textNodes = 1386 new ArrayList<>(); 1387 for (int i = 0; i < children.getLength(); i++) 1388 { 1389 final org.w3c.dom.Node nd = children.item(i); 1390 if (nd instanceof Text) 1391 { 1392 if (result == null) 1393 { 1394 result = (Text) nd; 1395 } 1396 else 1397 { 1398 textNodes.add(nd); 1399 } 1400 } 1401 } 1402 1403 // We don't want CDATAs 1404 if (result instanceof CDATASection) 1405 { 1406 textNodes.add(result); 1407 result = null; 1408 } 1409 1410 // Remove all but the first Text node 1411 for (final org.w3c.dom.Node tn : textNodes) 1412 { 1413 elem.removeChild(tn); 1414 } 1415 return result; 1416 } 1417 } 1418}