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