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