View Javadoc
1   /**
2    * This file Copyright (c) 2020 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.jackrabbit;
35  
36  import static java.util.Collections.emptyList;
37  
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.OutputStream;
41  import java.util.ArrayList;
42  import java.util.Collection;
43  import java.util.List;
44  import java.util.Optional;
45  import java.util.function.Consumer;
46  import java.util.function.Predicate;
47  
48  import javax.xml.parsers.DocumentBuilder;
49  import javax.xml.parsers.DocumentBuilderFactory;
50  import javax.xml.parsers.ParserConfigurationException;
51  import javax.xml.transform.OutputKeys;
52  import javax.xml.transform.Transformer;
53  import javax.xml.transform.TransformerException;
54  import javax.xml.transform.TransformerFactory;
55  import javax.xml.transform.dom.DOMSource;
56  import javax.xml.transform.stream.StreamResult;
57  
58  import org.w3c.dom.Document;
59  import org.w3c.dom.Element;
60  import org.w3c.dom.Node;
61  import org.w3c.dom.NodeList;
62  import org.xml.sax.SAXException;
63  
64  /**
65   * Helper class for creating an on the fly workspace.xml configurations from a template repository.xml
66   * and a stub workspace.xml.
67   */
68  public class WorkspaceConfigurationMerger {
69  
70      /**
71       * Create an on the fly {@code workspaceXml} configuration by merging elements from {@code repositoryXml}
72       * into {@code workspaceStubXml}:
73       *
74       * <ul>
75       * <li>Every direct child element of the {@code Workspace} element of {@code repositoryXml} is added as a direct
76       * child element to the {@code Workspace} element of {@code workspaceStubXml} unless an element of the same
77       * name already exists.</li>
78       * <li>No elements are added if a {@code Workspace} element does not exist in either  {@code repositoryXml} or
79       * {@code workspaceStubXml}.</li>
80       * <li>Every empty direct child element from thw {@code Workspace} element of {@code workspaceStubXml} is removed. </li>
81       * </ul>
82       *
83       * This arrangement allows inheriting, overriding and removing elements from {@code repositoryXml} in
84       * {@code workspaceStubXml}.
85       *
86       * @param repositoryXml     input stream containing a valid repository.xml. It is the responsibility of the caller
87       *                          to close this stream.
88       * @param workspaceStubXml  input stream containing a stub workspace.xml. It is the responsibility of the caller
89       *                          to close this stream.
90       * @param workspaceXml      output stream receiving the result of the merge operation. It is the responsibility of
91       *                          the caller to close this stream.
92       * @throws ParserConfigurationException when failing to acquire an XML parser
93       * @throws IOException when an IO error occurs during reading from the input streams
94       * @throws SAXException if any error occurs when parsing the xml from the input streams
95       * @throws TransformerException when an error occurs while during merging
96       */
97      public static void merge(InputStream repositoryXml, InputStream workspaceStubXml, OutputStream workspaceXml)
98              throws ParserConfigurationException, IOException, SAXException, TransformerException {
99  
100         DocumentBuilder xmlParser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
101         Document repositoryConfig = xmlParser.parse(repositoryXml);
102         Document workspaceConfig = xmlParser.parse(workspaceStubXml);
103 
104         getElement(workspaceConfig, "Workspace")
105 
106             // If the workspace configuration stub has a workspace element
107             .ifPresent(workspaceElement -> {
108 
109                 // the workspace configuration inherits all child elements from the Workspace element of the
110                 // repository configuration for which an element of the same name is not already present.
111                 getWorkspaceChildren(repositoryConfig).stream()
112                     .filter(notChildOf(workspaceElement))
113                     .forEach(addChild(workspaceConfig, workspaceElement));
114 
115                 // Remove all empty elements from the workspace configuration
116                 removeEmptyElements(workspaceElement);
117         });
118 
119         Transformer transformer = TransformerFactory.newInstance().newTransformer();
120         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
121         transformer.transform(new DOMSource(workspaceConfig), new StreamResult(workspaceXml));
122     }
123 
124     private static Predicate<Node> notChildOf(Element parent) {
125         return node -> !getElement(parent, node.getNodeName()).isPresent();
126     }
127 
128     private static Consumer<Node> addChild(Document document, Element parent) {
129         return node -> parent.appendChild(document.importNode(node, true));
130     }
131 
132     private static Collection<Node> getWorkspaceChildren(Document repositoryConfig) {
133         return getElement(repositoryConfig, "Repository")
134             .flatMap(repository -> getElement(repository, "Workspace"))
135             .map(WorkspaceConfigurationMerger::getChildNodes)
136             .orElse(emptyList());
137     }
138 
139     private static void removeEmptyElements(Node parent) {
140         getChildNodes(parent).stream()
141             .filter(WorkspaceConfigurationMerger::emptyElement)
142             .forEach(parent::removeChild);
143     }
144 
145     private static boolean emptyElement(Node node) {
146         return node.getNodeType() == Node.ELEMENT_NODE && !node.hasAttributes() && !node.hasChildNodes();
147     }
148 
149     private static Optional<Element> getElement(Node parent, String childName) {
150         return getChildNodes(parent).stream()
151             .filter(elementWithName(childName))
152             .findFirst()
153             .map(Element.class::cast);
154     }
155 
156     private static Predicate<Node> elementWithName(String childName) {
157         return node -> node.getNodeType() == Node.ELEMENT_NODE && childName.equals(node.getNodeName());
158     }
159 
160     private static Collection<Node> getChildNodes(Node parent) {
161         List<Node> nodes = new ArrayList<>();
162         NodeList children = parent.getChildNodes();
163         for (int i = 0; i < children.getLength(); i++) {
164             nodes.add(children.item(i));
165         }
166         return nodes;
167     }
168 
169 }