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 * http://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 package org.apache.commons.configuration2; 18 19 import java.util.Collections; 20 import java.util.HashMap; 21 import java.util.Map; 22 23 import javax.xml.parsers.DocumentBuilder; 24 import javax.xml.parsers.DocumentBuilderFactory; 25 import javax.xml.parsers.ParserConfigurationException; 26 import javax.xml.transform.Result; 27 import javax.xml.transform.Source; 28 import javax.xml.transform.Transformer; 29 import javax.xml.transform.TransformerConfigurationException; 30 import javax.xml.transform.TransformerException; 31 import javax.xml.transform.TransformerFactory; 32 import javax.xml.transform.dom.DOMResult; 33 import javax.xml.transform.dom.DOMSource; 34 35 import org.apache.commons.configuration2.ex.ConfigurationException; 36 import org.w3c.dom.Document; 37 import org.w3c.dom.Element; 38 import org.w3c.dom.Node; 39 import org.w3c.dom.NodeList; 40 41 /** 42 * <p> 43 * An internally used helper class for dealing with XML documents. 44 * </p> 45 * <p> 46 * This class is used by {@link XMLConfiguration}. It provides some basic functionality for processing DOM documents and 47 * dealing with elements. The main idea is that an instance holds the XML document associated with a XML configuration 48 * object. When the configuration is to be saved the document has to be manipulated according to the changes made on the 49 * configuration. To ensure that this is possible even under concurrent access, a new temporary instance is created as a 50 * copy of the original instance. Then, on this copy, the changes of the configuration are applied. The resulting 51 * document can then be serialized. 52 * </p> 53 * <p> 54 * Nodes of an {@code XMLConfiguration} that was read from a file are associated with the XML elements they represent. 55 * In order to apply changes on the copied document, it is necessary to establish a mapping between the elements of the 56 * old document and the elements of the copied document. This is also handled by this class. 57 * </p> 58 * 59 * @since 2.0 60 */ 61 final class XMLDocumentHelper { 62 /** Stores the document managed by this instance. */ 63 private final Document document; 64 65 /** The element mapping to the source document. */ 66 private final Map<Node, Node> elementMapping; 67 68 /** Stores the public ID of the source document. */ 69 private final String sourcePublicID; 70 71 /** Stores the system ID of the source document. */ 72 private final String sourceSystemID; 73 74 /** 75 * Creates a new instance of {@code XMLDocumentHelper} and initializes it with the given XML document. Note: This 76 * constructor is package private only for testing purposes. Instances should be created using the static factory 77 * methods. 78 * 79 * @param doc the {@code Document} 80 * @param elemMap the element mapping 81 * @param pubID the public ID of the source document 82 * @param sysID the system ID of the source document 83 */ 84 XMLDocumentHelper(final Document doc, final Map<Node, Node> elemMap, final String pubID, final String sysID) { 85 document = doc; 86 elementMapping = elemMap; 87 sourcePublicID = pubID; 88 sourceSystemID = sysID; 89 } 90 91 /** 92 * Creates a new instance of {@code XMLDocumentHelper} and initializes it with a newly created, empty {@code Document}. 93 * The new document has a root element with the given element name. This element has no further child nodes. 94 * 95 * @param rootElementName the name of the root element 96 * @return the newly created instance 97 * @throws ConfigurationException if an error occurs when creating the document 98 */ 99 public static XMLDocumentHelper forNewDocument(final String rootElementName) throws ConfigurationException { 100 final Document doc = createDocumentBuilder(createDocumentBuilderFactory()).newDocument(); 101 final Element rootElem = doc.createElement(rootElementName); 102 doc.appendChild(rootElem); 103 return new XMLDocumentHelper(doc, emptyElementMapping(), null, null); 104 } 105 106 /** 107 * Creates a new instance of {@code XMLDocumentHelper} and initializes it with a source document. This is a document 108 * created from a configuration file. It is kept in memory so that the configuration can be saved with the same format. 109 * Note that already a copy of this document is created. This is done for the following reasons: 110 * <ul> 111 * <li>It is a defensive copy.</li> 112 * <li>An identity transformation on a document may change certain nodes, e.g. CDATA sections. When later on again 113 * copies of this document are created it has to be ensured that these copies have the same structure than the original 114 * document stored in this instance.</li> 115 * </ul> 116 * 117 * @param srcDoc the source document 118 * @return the newly created instance 119 * @throws ConfigurationException if an error occurs 120 */ 121 public static XMLDocumentHelper forSourceDocument(final Document srcDoc) throws ConfigurationException { 122 final String pubID; 123 final String sysID; 124 if (srcDoc.getDoctype() != null) { 125 pubID = srcDoc.getDoctype().getPublicId(); 126 sysID = srcDoc.getDoctype().getSystemId(); 127 } else { 128 pubID = null; 129 sysID = null; 130 } 131 132 return new XMLDocumentHelper(copyDocument(srcDoc), emptyElementMapping(), pubID, sysID); 133 } 134 135 /** 136 * Gets the {@code Document} managed by this helper. 137 * 138 * @return the wrapped {@code Document} 139 */ 140 public Document getDocument() { 141 return document; 142 } 143 144 /** 145 * Gets the element mapping to the source document. This map can be used to obtain elements in the managed document 146 * which correspond to elements in the source document. If this instance has not been created from a source document, 147 * the mapping is empty. 148 * 149 * @return the element mapping to the source document 150 */ 151 public Map<Node, Node> getElementMapping() { 152 return elementMapping; 153 } 154 155 /** 156 * Gets the public ID of the source document. 157 * 158 * @return the public ID of the source document 159 */ 160 public String getSourcePublicID() { 161 return sourcePublicID; 162 } 163 164 /** 165 * Gets the system ID of the source document. 166 * 167 * @return the system ID of the source document 168 */ 169 public String getSourceSystemID() { 170 return sourceSystemID; 171 } 172 173 /** 174 * Creates a new {@code Transformer} object. No initializations are performed on the new instance. 175 * 176 * @return the new {@code Transformer} 177 * @throws ConfigurationException if the {@code Transformer} could not be created 178 */ 179 public static Transformer createTransformer() throws ConfigurationException { 180 return createTransformer(createTransformerFactory()); 181 } 182 183 /** 184 * Performs an XSL transformation on the passed in operands. All possible exceptions are caught and redirected as 185 * {@code ConfigurationException} exceptions. 186 * 187 * @param transformer the transformer 188 * @param source the source 189 * @param result the result 190 * @throws ConfigurationException if an error occurs 191 */ 192 public static void transform(final Transformer transformer, final Source source, final Result result) throws ConfigurationException { 193 try { 194 transformer.transform(source, result); 195 } catch (final TransformerException tex) { 196 throw new ConfigurationException(tex); 197 } 198 } 199 200 /** 201 * Creates a copy of this object. This copy contains a copy of the document and an element mapping which allows mapping 202 * elements from the source document to elements of the copied document. 203 * 204 * @return the copy 205 * @throws ConfigurationException if an error occurs 206 */ 207 public XMLDocumentHelper createCopy() throws ConfigurationException { 208 final Document docCopy = copyDocument(getDocument()); 209 return new XMLDocumentHelper(docCopy, createElementMapping(getDocument(), docCopy), getSourcePublicID(), getSourceSystemID()); 210 } 211 212 /** 213 * Creates a new {@code TransformerFactory}. 214 * 215 * @return the {@code TransformerFactory} 216 */ 217 static TransformerFactory createTransformerFactory() { 218 return TransformerFactory.newInstance(); 219 } 220 221 /** 222 * Creates a {@code Transformer} using the specified factory. 223 * 224 * @param factory the {@code TransformerFactory} 225 * @return the newly created {@code Transformer} 226 * @throws ConfigurationException if an error occurs 227 */ 228 static Transformer createTransformer(final TransformerFactory factory) throws ConfigurationException { 229 try { 230 return factory.newTransformer(); 231 } catch (final TransformerConfigurationException tex) { 232 throw new ConfigurationException(tex); 233 } 234 } 235 236 /** 237 * Creates a new {@code DocumentBuilder} using the specified factory. Exceptions are rethrown as 238 * {@code ConfigurationException} exceptions. 239 * 240 * @param factory the {@code DocumentBuilderFactory} 241 * @return the newly created {@code DocumentBuilder} 242 * @throws ConfigurationException if an error occurs 243 */ 244 static DocumentBuilder createDocumentBuilder(final DocumentBuilderFactory factory) throws ConfigurationException { 245 try { 246 return factory.newDocumentBuilder(); 247 } catch (final ParserConfigurationException pcex) { 248 throw new ConfigurationException(pcex); 249 } 250 } 251 252 /** 253 * Creates a copy of the specified document. 254 * 255 * @param doc the {@code Document} 256 * @return the copy of this document 257 * @throws ConfigurationException if an error occurs 258 */ 259 private static Document copyDocument(final Document doc) throws ConfigurationException { 260 final Transformer transformer = createTransformer(); 261 final DOMSource source = new DOMSource(doc); 262 final DOMResult result = new DOMResult(); 263 transform(transformer, source, result); 264 265 return (Document) result.getNode(); 266 } 267 268 /** 269 * Creates a new {@code DocumentBuilderFactory} instance. 270 * 271 * @return the new factory object 272 */ 273 private static DocumentBuilderFactory createDocumentBuilderFactory() { 274 return DocumentBuilderFactory.newInstance(); 275 } 276 277 /** 278 * Creates an empty element mapping. 279 * 280 * @return the empty mapping 281 */ 282 private static Map<Node, Node> emptyElementMapping() { 283 return Collections.emptyMap(); 284 } 285 286 /** 287 * Creates the element mapping for the specified documents. For each node in the source document an entry is created 288 * pointing to the corresponding node in the destination object. 289 * 290 * @param doc1 the source document 291 * @param doc2 the destination document 292 * @return the element mapping 293 */ 294 private static Map<Node, Node> createElementMapping(final Document doc1, final Document doc2) { 295 final Map<Node, Node> mapping = new HashMap<>(); 296 createElementMappingForNodes(doc1.getDocumentElement(), doc2.getDocumentElement(), mapping); 297 return mapping; 298 } 299 300 /** 301 * Creates the element mapping for the specified nodes and all their child nodes. 302 * 303 * @param n1 node 1 304 * @param n2 node 2 305 * @param mapping the mapping to be filled 306 */ 307 private static void createElementMappingForNodes(final Node n1, final Node n2, final Map<Node, Node> mapping) { 308 mapping.put(n1, n2); 309 final NodeList childNodes1 = n1.getChildNodes(); 310 final NodeList childNodes2 = n2.getChildNodes(); 311 final int count = Math.min(childNodes1.getLength(), childNodes2.getLength()); 312 for (int i = 0; i < count; i++) { 313 createElementMappingForNodes(childNodes1.item(i), childNodes2.item(i), mapping); 314 } 315 } 316 }