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