1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.configuration2;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.Reader;
23 import java.io.StringReader;
24 import java.io.StringWriter;
25 import java.io.Writer;
26 import java.net.URL;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.Map;
33
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 import javax.xml.transform.OutputKeys;
38 import javax.xml.transform.Result;
39 import javax.xml.transform.Source;
40 import javax.xml.transform.Transformer;
41 import javax.xml.transform.dom.DOMSource;
42 import javax.xml.transform.stream.StreamResult;
43
44 import org.apache.commons.configuration2.convert.ListDelimiterHandler;
45 import org.apache.commons.configuration2.ex.ConfigurationException;
46 import org.apache.commons.configuration2.io.ConfigurationLogger;
47 import org.apache.commons.configuration2.io.FileLocator;
48 import org.apache.commons.configuration2.io.FileLocatorAware;
49 import org.apache.commons.configuration2.io.InputStreamSupport;
50 import org.apache.commons.configuration2.resolver.DefaultEntityResolver;
51 import org.apache.commons.configuration2.tree.ImmutableNode;
52 import org.apache.commons.configuration2.tree.NodeTreeWalker;
53 import org.apache.commons.configuration2.tree.ReferenceNodeHandler;
54 import org.apache.commons.lang3.StringUtils;
55 import org.apache.commons.lang3.mutable.MutableObject;
56 import org.w3c.dom.Attr;
57 import org.w3c.dom.CDATASection;
58 import org.w3c.dom.Document;
59 import org.w3c.dom.Element;
60 import org.w3c.dom.NamedNodeMap;
61 import org.w3c.dom.Node;
62 import org.w3c.dom.NodeList;
63 import org.w3c.dom.Text;
64 import org.xml.sax.EntityResolver;
65 import org.xml.sax.InputSource;
66 import org.xml.sax.SAXException;
67 import org.xml.sax.SAXParseException;
68 import org.xml.sax.helpers.DefaultHandler;
69
70 /**
71 * <p>
72 * A specialized hierarchical configuration class that is able to parse XML documents.
73 * </p>
74 * <p>
75 * The parsed document will be stored keeping its structure. The class also tries to preserve as much information from
76 * the loaded XML document as possible, including comments and processing instructions. These will be contained in
77 * documents created by the {@code save()} methods, too.
78 * </p>
79 * <p>
80 * Like other file based configuration classes this class maintains the name and path to the loaded configuration file.
81 * These properties can be altered using several setter methods, but they are not modified by {@code save()} and
82 * {@code load()} methods. If XML documents contain relative paths to other documents (for example to a DTD), these references
83 * are resolved based on the path set for this configuration.
84 * </p>
85 * <p>
86 * By inheriting from {@link AbstractConfiguration} this class provides some extended functionality, for example interpolation
87 * of property values. Like in {@link PropertiesConfiguration} property values can contain delimiter characters (the
88 * comma ',' per default) and are then split into multiple values. This works for XML attributes and text content of
89 * elements as well. The delimiter can be escaped by a backslash. As an example consider the following XML fragment:
90 * </p>
91 *
92 * <pre>
93 * <config>
94 * <array>10,20,30,40</array>
95 * <scalar>3\,1415</scalar>
96 * <cite text="To be or not to be\, this is the question!"/>
97 * </config>
98 * </pre>
99 *
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 */
171 public 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 }