Fixing XML parsers sonar issue
[so.git] / bpmn / MSOCoreBPMN / src / main / java / org / onap / so / bpmn / core / xml / XmlTool.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved.
7  * ================================================================================
8  * Modifications Copyright (c) 2019 Samsung
9  * ================================================================================
10  * Licensed under the Apache License, Version 2.0 (the "License");
11  * you may not use this file except in compliance with the License.
12  * You may obtain a copy of the License at
13  * 
14  *      http://www.apache.org/licenses/LICENSE-2.0
15  * 
16  * Unless required by applicable law or agreed to in writing, software
17  * distributed under the License is distributed on an "AS IS" BASIS,
18  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19  * See the License for the specific language governing permissions and
20  * limitations under the License.
21  * ============LICENSE_END=========================================================
22  */
23
24 package org.onap.so.bpmn.core.xml;
25
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.Reader;
30 import java.io.StringReader;
31 import java.io.StringWriter;
32 import java.util.HashMap;
33 import java.util.Map;
34 import java.util.Optional;
35 import javax.xml.XMLConstants;
36 import javax.xml.parsers.DocumentBuilder;
37 import javax.xml.parsers.DocumentBuilderFactory;
38 import javax.xml.parsers.ParserConfigurationException;
39 import javax.xml.transform.OutputKeys;
40 import javax.xml.transform.Source;
41 import javax.xml.transform.Transformer;
42 import javax.xml.transform.TransformerException;
43 import javax.xml.transform.TransformerFactory;
44 import javax.xml.transform.dom.DOMSource;
45 import javax.xml.transform.stream.StreamResult;
46 import javax.xml.transform.stream.StreamSource;
47 import javax.xml.xpath.XPath;
48 import javax.xml.xpath.XPathConstants;
49 import javax.xml.xpath.XPathExpressionException;
50 import javax.xml.xpath.XPathFactory;
51 import org.apache.commons.lang3.StringEscapeUtils;
52 import org.apache.commons.lang3.StringUtils;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55 import org.w3c.dom.Document;
56 import org.w3c.dom.Node;
57 import org.w3c.dom.NodeList;
58 import org.xml.sax.InputSource;
59 import org.xml.sax.SAXException;
60
61 /**
62  * XML transformation methods and other useful functions.
63  */
64 public final class XmlTool {
65
66     private static final Map<String, Integer> ENTITIES = new HashMap<>();
67     private static final Logger logger = LoggerFactory.getLogger(XmlTool.class);
68     static {
69         ENTITIES.put("amp", 38);
70         ENTITIES.put("quot", 34);
71         ENTITIES.put("lt", 60);
72         ENTITIES.put("gt", 62);
73     }
74
75     /**
76      * Instantiation is not allowed.
77      */
78     private XmlTool() {}
79
80     /**
81      * Normalizes and formats XML. This method consolidates and moves all namespace declarations to the root element.
82      * The result will not have an XML prolog or a trailing newline.
83      * 
84      * @param xml the XML to normalize
85      * @throws IOException
86      * @throws TransformerException
87      * @throws ParserConfigurationException
88      * @throws SAXException
89      * @throws XPathExpressionException
90      */
91     public static String normalize(final Object xml) throws IOException, TransformerException,
92             ParserConfigurationException, SAXException, XPathExpressionException {
93
94         if (xml == null) {
95             return null;
96         }
97
98         final Source xsltSource = new StreamSource(new StringReader(readResourceFile("normalize-namespaces.xsl")));
99
100         final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
101         dbFactory.setNamespaceAware(true);
102         dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
103         dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
104         final DocumentBuilder db = dbFactory.newDocumentBuilder();
105         final InputSource source = new InputSource(new StringReader(String.valueOf(xml)));
106         final Document doc = db.parse(source);
107
108         // Start of code to remove whitespace outside of tags
109         final XPath xPath = XPathFactory.newInstance().newXPath();
110         final NodeList nodeList =
111                 (NodeList) xPath.evaluate("//text()[normalize-space()='']", doc, XPathConstants.NODESET);
112
113         for (int i = 0; i < nodeList.getLength(); ++i) {
114             final Node node = nodeList.item(i);
115             node.getParentNode().removeChild(node);
116         }
117         // End of code to remove whitespace outside of tags
118
119         // the factory pattern supports different XSLT processors
120         final TransformerFactory transformerFactory = TransformerFactory.newInstance();
121         transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, StringUtils.EMPTY);
122         transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, StringUtils.EMPTY);
123         transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
124         final Transformer transformer = transformerFactory.newTransformer(xsltSource);
125
126         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
127         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
128         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
129         transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
130
131         final StringWriter writer = new StringWriter();
132         transformer.transform(new DOMSource(doc), new StreamResult(writer));
133         return writer.toString().trim();
134     }
135
136     /**
137      * Encodes a value so it can be used inside an XML text element.
138      * 
139      * @param value the string to encode
140      * @return the encoded string
141      */
142     public static String encode(Object value) {
143         if (value == null) {
144             return null;
145         }
146         return StringEscapeUtils.escapeXml11(value.toString());
147     }
148
149     /**
150      * Removes the preamble, if present, from an XML document.
151      * 
152      * @param xml the XML document
153      * @return a possibly modified document
154      */
155     public static String removePreamble(Object xml) {
156         if (xml == null) {
157             return null;
158         }
159
160         return String.valueOf(xml).replaceAll("(<\\?[^<]*\\?>\\s*[\\r\\n]*)?", "");
161     }
162
163     /**
164      * Removes namespaces and namespace declarations from an XML document.
165      * 
166      * @param xml the XML document
167      * @return a possibly modified document
168      */
169     public static String removeNamespaces(Object xml) {
170         if (xml == null) {
171             logger.debug("removeNamespaces input object is null , returning null");
172             return null;
173         }
174
175         String text = String.valueOf(xml);
176
177         // remove xmlns declaration
178         text = text.replaceAll("xmlns.*?(\"|\').*?(\"|\')", "");
179         // remove opening tag prefix
180         text = text.replaceAll("(<)(\\w+:)(.*?>)", "$1$3");
181         // remove closing tags prefix
182         text = text.replaceAll("(</)(\\w+:)(.*?>)", "$1$3");
183         // remove extra spaces left when xmlns declarations are removed
184         text = text.replaceAll("\\s+>", ">");
185
186         return text;
187     }
188
189
190     /**
191      * Reads the specified resource file and return the contents as a string.
192      * 
193      * @param file Name of the resource file
194      * @return the contents of the resource file as a String
195      * @throws IOException if there is a problem reading the file
196      */
197     private static String readResourceFile(String file) throws IOException {
198
199         try (InputStream stream = XmlTool.class.getClassLoader().getResourceAsStream(file);
200                 Reader reader = new InputStreamReader(stream, "UTF-8")) {
201
202             StringBuilder out = new StringBuilder();
203             char[] buf = new char[1024];
204             int n;
205
206             while ((n = reader.read(buf)) >= 0) {
207                 out.append(buf, 0, n);
208             }
209             return out.toString();
210         } catch (Exception e) {
211             logger.debug("Exception at readResourceFile stream: " + e);
212             return null;
213         }
214     }
215
216     /**
217      * Parses the XML document String for the first occurrence of the specified element tag. If found, the value
218      * associated with that element tag is replaced with the new value and a String containing the modified XML document
219      * is returned. If the XML passed is null or the element tag is not found in the document, null will be returned.
220      * 
221      * @param xml String containing the original XML document.
222      * @param elementTag String containing the tag of the element to be modified.
223      * @param newValue String containing the new value to be used to modify the corresponding element.
224      * @return the contents of the modified XML document as a String or null/empty if the modification failed.
225      * @throws IOException, TransformerException, ParserConfigurationException, SAXException
226      */
227     public static Optional<String> modifyElement(String xml, String elementTag, String newValue)
228             throws IOException, TransformerException, ParserConfigurationException, SAXException {
229
230         if (xml == null || xml.isEmpty()) {
231             // no XML content to be modified, return empty
232             return Optional.empty();
233         }
234
235         DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
236         dbFactory.setNamespaceAware(true);
237         dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
238         dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
239         DocumentBuilder db = dbFactory.newDocumentBuilder();
240         InputSource source = new InputSource(new StringReader(xml));
241         Document doc = db.parse(source);
242
243         Node modNode = doc.getElementsByTagName(elementTag).item(0);
244         if (modNode == null) {
245             // did not find the specified element to be modified, return empty
246             // System.out.println("Did not find element tag " + elementTag + " in XML");
247             return Optional.empty();
248         } else {
249             modNode.setTextContent(newValue);
250         }
251
252         TransformerFactory transformerFactory = TransformerFactory.newInstance();
253         Transformer transformer = transformerFactory.newTransformer();
254         StringWriter writer = new StringWriter();
255         transformer.transform(new DOMSource(doc), new StreamResult(writer));
256         // return the modified String representation of the XML
257         return Optional.of(writer.toString().trim());
258     }
259 }