XMLDocumentHelper.java

  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. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.Map;

  21. import javax.xml.parsers.DocumentBuilder;
  22. import javax.xml.parsers.DocumentBuilderFactory;
  23. import javax.xml.parsers.ParserConfigurationException;
  24. import javax.xml.transform.Result;
  25. import javax.xml.transform.Source;
  26. import javax.xml.transform.Transformer;
  27. import javax.xml.transform.TransformerConfigurationException;
  28. import javax.xml.transform.TransformerException;
  29. import javax.xml.transform.TransformerFactory;
  30. import javax.xml.transform.dom.DOMResult;
  31. import javax.xml.transform.dom.DOMSource;

  32. import org.apache.commons.configuration2.ex.ConfigurationException;
  33. import org.w3c.dom.Document;
  34. import org.w3c.dom.Element;
  35. import org.w3c.dom.Node;
  36. import org.w3c.dom.NodeList;

  37. /**
  38.  * <p>
  39.  * An internally used helper class for dealing with XML documents.
  40.  * </p>
  41.  * <p>
  42.  * This class is used by {@link XMLConfiguration}. It provides some basic functionality for processing DOM documents and
  43.  * dealing with elements. The main idea is that an instance holds the XML document associated with a XML configuration
  44.  * object. When the configuration is to be saved the document has to be manipulated according to the changes made on the
  45.  * configuration. To ensure that this is possible even under concurrent access, a new temporary instance is created as a
  46.  * copy of the original instance. Then, on this copy, the changes of the configuration are applied. The resulting
  47.  * document can then be serialized.
  48.  * </p>
  49.  * <p>
  50.  * Nodes of an {@code XMLConfiguration} that was read from a file are associated with the XML elements they represent.
  51.  * In order to apply changes on the copied document, it is necessary to establish a mapping between the elements of the
  52.  * old document and the elements of the copied document. This is also handled by this class.
  53.  * </p>
  54.  *
  55.  * @since 2.0
  56.  */
  57. final class XMLDocumentHelper {
  58.     /**
  59.      * Creates a copy of the specified document.
  60.      *
  61.      * @param doc the {@code Document}
  62.      * @return the copy of this document
  63.      * @throws ConfigurationException if an error occurs
  64.      */
  65.     private static Document copyDocument(final Document doc) throws ConfigurationException {
  66.         final Transformer transformer = createTransformer();
  67.         final DOMSource source = new DOMSource(doc);
  68.         final DOMResult result = new DOMResult();
  69.         transform(transformer, source, result);

  70.         return (Document) result.getNode();
  71.     }

  72.     /**
  73.      * Creates a new {@code DocumentBuilder} using the specified factory. Exceptions are rethrown as
  74.      * {@code ConfigurationException} exceptions.
  75.      *
  76.      * @param factory the {@code DocumentBuilderFactory}
  77.      * @return the newly created {@code DocumentBuilder}
  78.      * @throws ConfigurationException if an error occurs
  79.      */
  80.     static DocumentBuilder createDocumentBuilder(final DocumentBuilderFactory factory) throws ConfigurationException {
  81.         try {
  82.             return factory.newDocumentBuilder();
  83.         } catch (final ParserConfigurationException pcex) {
  84.             throw new ConfigurationException(pcex);
  85.         }
  86.     }

  87.     /**
  88.      * Creates a new {@code DocumentBuilderFactory} instance.
  89.      *
  90.      * @return the new factory object
  91.      */
  92.     private static DocumentBuilderFactory createDocumentBuilderFactory() {
  93.         return DocumentBuilderFactory.newInstance();
  94.     }

  95.     /**
  96.      * Creates the element mapping for the specified documents. For each node in the source document an entry is created
  97.      * pointing to the corresponding node in the destination object.
  98.      *
  99.      * @param doc1 the source document
  100.      * @param doc2 the destination document
  101.      * @return the element mapping
  102.      */
  103.     private static Map<Node, Node> createElementMapping(final Document doc1, final Document doc2) {
  104.         final Map<Node, Node> mapping = new HashMap<>();
  105.         createElementMappingForNodes(doc1.getDocumentElement(), doc2.getDocumentElement(), mapping);
  106.         return mapping;
  107.     }

  108.     /**
  109.      * Creates the element mapping for the specified nodes and all their child nodes.
  110.      *
  111.      * @param n1 node 1
  112.      * @param n2 node 2
  113.      * @param mapping the mapping to be filled
  114.      */
  115.     private static void createElementMappingForNodes(final Node n1, final Node n2, final Map<Node, Node> mapping) {
  116.         mapping.put(n1, n2);
  117.         final NodeList childNodes1 = n1.getChildNodes();
  118.         final NodeList childNodes2 = n2.getChildNodes();
  119.         final int count = Math.min(childNodes1.getLength(), childNodes2.getLength());
  120.         for (int i = 0; i < count; i++) {
  121.             createElementMappingForNodes(childNodes1.item(i), childNodes2.item(i), mapping);
  122.         }
  123.     }

  124.     /**
  125.      * Creates a new {@code Transformer} object. No initializations are performed on the new instance.
  126.      *
  127.      * @return the new {@code Transformer}
  128.      * @throws ConfigurationException if the {@code Transformer} could not be created
  129.      */
  130.     public static Transformer createTransformer() throws ConfigurationException {
  131.         return createTransformer(createTransformerFactory());
  132.     }

  133.     /**
  134.      * Creates a {@code Transformer} using the specified factory.
  135.      *
  136.      * @param factory the {@code TransformerFactory}
  137.      * @return the newly created {@code Transformer}
  138.      * @throws ConfigurationException if an error occurs
  139.      */
  140.     static Transformer createTransformer(final TransformerFactory factory) throws ConfigurationException {
  141.         try {
  142.             return factory.newTransformer();
  143.         } catch (final TransformerConfigurationException tex) {
  144.             throw new ConfigurationException(tex);
  145.         }
  146.     }

  147.     /**
  148.      * Creates a new {@code TransformerFactory}.
  149.      *
  150.      * @return the {@code TransformerFactory}
  151.      */
  152.     static TransformerFactory createTransformerFactory() {
  153.         return TransformerFactory.newInstance();
  154.     }

  155.     /**
  156.      * Creates an empty element mapping.
  157.      *
  158.      * @return the empty mapping
  159.      */
  160.     private static Map<Node, Node> emptyElementMapping() {
  161.         return Collections.emptyMap();
  162.     }

  163.     /**
  164.      * Creates a new instance of {@code XMLDocumentHelper} and initializes it with a newly created, empty {@code Document}.
  165.      * The new document has a root element with the given element name. This element has no further child nodes.
  166.      *
  167.      * @param rootElementName the name of the root element
  168.      * @return the newly created instance
  169.      * @throws ConfigurationException if an error occurs when creating the document
  170.      */
  171.     public static XMLDocumentHelper forNewDocument(final String rootElementName) throws ConfigurationException {
  172.         final Document doc = createDocumentBuilder(createDocumentBuilderFactory()).newDocument();
  173.         final Element rootElem = doc.createElement(rootElementName);
  174.         doc.appendChild(rootElem);
  175.         return new XMLDocumentHelper(doc, emptyElementMapping(), null, null);
  176.     }

  177.     /**
  178.      * Creates a new instance of {@code XMLDocumentHelper} and initializes it with a source document. This is a document
  179.      * created from a configuration file. It is kept in memory so that the configuration can be saved with the same format.
  180.      * Note that already a copy of this document is created. This is done for the following reasons:
  181.      * <ul>
  182.      * <li>It is a defensive copy.</li>
  183.      * <li>An identity transformation on a document may change certain nodes, for example CDATA sections. When later on again
  184.      * copies of this document are created it has to be ensured that these copies have the same structure than the original
  185.      * document stored in this instance.</li>
  186.      * </ul>
  187.      *
  188.      * @param srcDoc the source document
  189.      * @return the newly created instance
  190.      * @throws ConfigurationException if an error occurs
  191.      */
  192.     public static XMLDocumentHelper forSourceDocument(final Document srcDoc) throws ConfigurationException {
  193.         final String pubID;
  194.         final String sysID;
  195.         if (srcDoc.getDoctype() != null) {
  196.             pubID = srcDoc.getDoctype().getPublicId();
  197.             sysID = srcDoc.getDoctype().getSystemId();
  198.         } else {
  199.             pubID = null;
  200.             sysID = null;
  201.         }

  202.         return new XMLDocumentHelper(copyDocument(srcDoc), emptyElementMapping(), pubID, sysID);
  203.     }

  204.     /**
  205.      * Performs an XSL transformation on the passed in operands. All possible exceptions are caught and redirected as
  206.      * {@code ConfigurationException} exceptions.
  207.      *
  208.      * @param transformer the transformer
  209.      * @param source the source
  210.      * @param result the result
  211.      * @throws ConfigurationException if an error occurs
  212.      */
  213.     public static void transform(final Transformer transformer, final Source source, final Result result) throws ConfigurationException {
  214.         try {
  215.             transformer.transform(source, result);
  216.         } catch (final TransformerException tex) {
  217.             throw new ConfigurationException(tex);
  218.         }
  219.     }

  220.     /** Stores the document managed by this instance. */
  221.     private final Document document;

  222.     /** The element mapping to the source document. */
  223.     private final Map<Node, Node> elementMapping;

  224.     /** Stores the public ID of the source document. */
  225.     private final String sourcePublicID;

  226.     /** Stores the system ID of the source document. */
  227.     private final String sourceSystemID;

  228.     /**
  229.      * Creates a new instance of {@code XMLDocumentHelper} and initializes it with the given XML document. Note: This
  230.      * constructor is package private only for testing purposes. Instances should be created using the static factory
  231.      * methods.
  232.      *
  233.      * @param doc the {@code Document}
  234.      * @param elemMap the element mapping
  235.      * @param pubID the public ID of the source document
  236.      * @param sysID the system ID of the source document
  237.      */
  238.     XMLDocumentHelper(final Document doc, final Map<Node, Node> elemMap, final String pubID, final String sysID) {
  239.         document = doc;
  240.         elementMapping = elemMap;
  241.         sourcePublicID = pubID;
  242.         sourceSystemID = sysID;
  243.     }

  244.     /**
  245.      * Creates a copy of this object. This copy contains a copy of the document and an element mapping which allows mapping
  246.      * elements from the source document to elements of the copied document.
  247.      *
  248.      * @return the copy
  249.      * @throws ConfigurationException if an error occurs
  250.      */
  251.     public XMLDocumentHelper createCopy() throws ConfigurationException {
  252.         final Document docCopy = copyDocument(getDocument());
  253.         return new XMLDocumentHelper(docCopy, createElementMapping(getDocument(), docCopy), getSourcePublicID(), getSourceSystemID());
  254.     }

  255.     /**
  256.      * Gets the {@code Document} managed by this helper.
  257.      *
  258.      * @return the wrapped {@code Document}
  259.      */
  260.     public Document getDocument() {
  261.         return document;
  262.     }

  263.     /**
  264.      * Gets the element mapping to the source document. This map can be used to obtain elements in the managed document
  265.      * which correspond to elements in the source document. If this instance has not been created from a source document,
  266.      * the mapping is empty.
  267.      *
  268.      * @return the element mapping to the source document
  269.      */
  270.     public Map<Node, Node> getElementMapping() {
  271.         return elementMapping;
  272.     }

  273.     /**
  274.      * Gets the public ID of the source document.
  275.      *
  276.      * @return the public ID of the source document
  277.      */
  278.     public String getSourcePublicID() {
  279.         return sourcePublicID;
  280.     }

  281.     /**
  282.      * Gets the system ID of the source document.
  283.      *
  284.      * @return the system ID of the source document
  285.      */
  286.     public String getSourceSystemID() {
  287.         return sourceSystemID;
  288.     }
  289. }