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