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