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