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