1710 Rebase - Second Attempt
[so.git] / bpmn / MSOCoreBPMN / src / main / java / org / openecomp / mso / 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  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.mso.bpmn.core.xml;
22
23 import java.io.FileNotFoundException;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.Reader;
28 import java.io.StringReader;
29 import java.io.StringWriter;
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Optional;
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.Source;
39 import javax.xml.transform.Transformer;
40 import javax.xml.transform.TransformerException;
41 import javax.xml.transform.TransformerFactory;
42 import javax.xml.transform.dom.DOMSource;
43 import javax.xml.transform.stream.StreamResult;
44 import javax.xml.transform.stream.StreamSource;
45 import javax.xml.xpath.XPath;
46 import javax.xml.xpath.XPathConstants;
47 import javax.xml.xpath.XPathExpressionException;
48 import javax.xml.xpath.XPathFactory;
49
50 import org.openecomp.mso.logger.MsoLogger;
51 import org.w3c.dom.Document;
52 import org.w3c.dom.NamedNodeMap;
53 import org.w3c.dom.Node;
54 import org.w3c.dom.NodeList;
55 import org.xml.sax.InputSource;
56 import org.xml.sax.SAXException;
57
58 /**
59  * XML transformation methods and other useful functions.
60  */
61 public final class XmlTool {
62
63         private static final Map<String, Integer> ENTITIES = new HashMap<String, Integer>();
64         private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.BPEL);
65         static {
66                 ENTITIES.put("amp", new Integer(38));
67                 ENTITIES.put("quot", new Integer(34));
68                 ENTITIES.put("lt", new Integer(60));
69                 ENTITIES.put("gt", new Integer(62));
70         }
71
72         /**
73          * Normalizes and formats XML.  This method consolidates and moves all namespace
74          * declarations to the root element.  The result will not have an XML prolog or
75          * a trailing newline.
76          * @param xml the XML to normalize
77          * @throws IOException 
78          * @throws TransformerException 
79          * @throws ParserConfigurationException 
80          * @throws SAXException 
81          * @throws XPathExpressionException 
82          */
83         public static String normalize(Object xml) throws IOException, TransformerException,
84                         ParserConfigurationException, SAXException, XPathExpressionException {
85                 
86                 if (xml == null) {
87                         return null;
88                 }
89
90                 Source xsltSource = new StreamSource(new StringReader(
91                         readResourceFile("normalize-namespaces.xsl")));
92
93                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
94                 dbFactory.setNamespaceAware(true);
95                 DocumentBuilder db = dbFactory.newDocumentBuilder();
96                 InputSource source = new InputSource(new StringReader(String.valueOf(xml)));
97                 Document doc = db.parse(source);
98
99                 // Start of code to remove whitespace outside of tags
100                 XPath xPath = XPathFactory.newInstance().newXPath();
101                 NodeList nodeList = (NodeList) xPath.evaluate(
102                         "//text()[normalize-space()='']", doc, XPathConstants.NODESET);
103
104                 for (int i = 0; i < nodeList.getLength(); ++i) {
105                         Node node = nodeList.item(i);
106                         node.getParentNode().removeChild(node);
107                 }
108                 // End of code to remove whitespace outside of tags
109
110                 // the factory pattern supports different XSLT processors
111                 TransformerFactory transformerFactory = TransformerFactory.newInstance();
112                 Transformer transformer = transformerFactory.newTransformer(xsltSource);
113
114                 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
115                 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
116                 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
117                 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
118
119                 StringWriter writer = new StringWriter();
120                 transformer.transform(new DOMSource(doc), new StreamResult(writer));
121                 return writer.toString().trim();
122         }
123
124         /**
125          * Encodes a value so it can be used inside an XML text element.
126          * @param s the string to encode
127          * @return the encoded string
128          */
129         public static String encode(Object value) {
130                 if (value == null) {
131                         return null;
132                 }
133
134                 String s = String.valueOf(value);
135                 StringBuilder out = new StringBuilder();
136                 boolean modified = false;
137
138                 for (int i = 0; i < s.length(); i++) {
139                         char c = s.charAt(i);
140
141                         if (c == '<') {
142                                 out.append("&lt;");
143                                 modified = true;
144                         } else if (c == '>') {
145                                 out.append("&gt;");
146                                 modified = true;
147                         } else if (c == '&') {
148                                 out.append("&amp;");
149                                 modified = true;
150                         } else if (c < 32 || c > 126) {
151                                 out.append("&#" + (int)c + ";");
152                                 modified = true;
153                         } else {
154                                 out.append(c);
155                         }
156                 }
157
158                 if (modified) {
159                         return out.toString();
160                 } else {
161                         return s;
162                 }
163         }
164         
165         /**
166          * Encodes a value so it can be used inside an XML attribute.
167          * @param s the string to encode
168          * @return the encoded string
169          */
170         public static String encodeAttr(Object value) {
171                 if (value == null) {
172                         return null;
173                 }
174
175                 String s = String.valueOf(value);
176                 StringBuilder out = new StringBuilder();
177                 boolean modified = false;
178
179                 for (int i = 0; i < s.length(); i++) {
180                         char c = s.charAt(i);
181
182                         if (c == '<') {
183                                 out.append("&lt;");
184                                 modified = true;
185                         } else if (c == '>') {
186                                 out.append("&gt;");
187                                 modified = true;
188                         } else if (c == '"') {
189                                 out.append("&quot;");
190                                 modified = true;
191                         } else if (c == '&') {
192                                 out.append("&amp;");
193                                 modified = true;
194                         } else if (c < 32 || c > 126) {
195                                 out.append("&#" + (int)c + ";");
196                                 modified = true;
197                         } else {
198                                 out.append(c);
199                         }
200                 }
201
202                 if (modified) {
203                         return out.toString();
204                 } else {
205                         return s;
206                 }
207         }
208         
209         /**
210          * Decodes XML entities in a string value
211          * @param value a value with embedded XML entities
212          * @return the decoded string
213          */
214         public static String decode(Object value) {
215                 if (value == null) {
216                         return null;
217                 }
218                 
219                 String s = String.valueOf(value);
220
221                 StringBuilder out = new StringBuilder(s.length());
222                 int ampIndex = s.indexOf("&");
223                 int lastEnd = 0;
224
225                 while (ampIndex >= 0) {
226                         int nextAmpIndex = s.indexOf("&", ampIndex + 1);
227                         int nextSemiIndex = s.indexOf(";", ampIndex + 1);
228                         if (nextSemiIndex != -1 && (nextAmpIndex == -1 || nextSemiIndex < nextAmpIndex)) {
229                                 int code = -1;
230                                 String entity = s.substring(ampIndex + 1, nextSemiIndex);
231
232                                 try {
233                                         if (entity.startsWith("#")) {
234                                                 code = Integer.parseInt(entity.substring(1), 10);
235                                         } else {
236                                                 if (ENTITIES.containsKey(entity)) {
237                                                         code = ENTITIES.get(entity);
238                                                 }
239                                         }
240                                 } catch (NumberFormatException x) {
241                                         // Do nothing
242                                 }
243
244                                 out.append(s.substring(lastEnd, ampIndex));
245                                 lastEnd = nextSemiIndex + 1;
246                                 if (code >= 0 && code <= 0xffff) {
247                                         out.append((char) code);
248                                 } else {
249                                         out.append("&");
250                                         out.append(entity);
251                                         out.append(";");
252                                 }
253                         }
254
255                         ampIndex = nextAmpIndex;
256                 }
257
258                 out.append(s.substring(lastEnd));
259                 return out.toString();
260         }
261
262         /**
263          * Removes the preamble, if present, from an XML document.
264          * @param xml the XML document
265          * @return a possibly modified document
266          */
267         public static String removePreamble(Object xml) {
268                 if (xml == null) {
269                         return null;
270                 }
271
272                 return String.valueOf(xml).replaceAll("(<\\?[^<]*\\?>\\s*[\\r\\n]*)?", "");
273         }
274
275         /**
276          * Removes namespaces and namespace declarations from an XML document.
277          * @param xml the XML document
278          * @return a possibly modified document
279          */
280         public static String removeNamespaces(Object xml) {
281                 if (xml == null) {
282                 LOGGER.debug("removeNamespaces input object is null , returning null");
283                         return null;
284                 }
285
286                 String text = String.valueOf(xml);
287
288                 // remove xmlns declaration
289                 text = text.replaceAll("xmlns.*?(\"|\').*?(\"|\')", "");
290                 // remove opening tag prefix
291                 text = text.replaceAll("(<)(\\w+:)(.*?>)", "$1$3");
292                 // remove closing tags prefix
293                 text = text.replaceAll("(</)(\\w+:)(.*?>)", "$1$3");
294                 // remove extra spaces left when xmlns declarations are removed
295                 text = text.replaceAll("\\s+>", ">");
296
297                 return text;
298         }
299
300
301         /**
302          * Reads the specified resource file and return the contents as a string.
303          * @param file Name of the resource file
304          * @return the contents of the resource file as a String
305          * @throws IOException if there is a problem reading the file
306          */
307         private static String readResourceFile(String file) throws IOException {
308                 InputStream stream = null;
309                 try {
310                         stream = XmlTool.class.getClassLoader().getResourceAsStream(file);
311
312                         if (stream == null) {
313                                 throw new FileNotFoundException("No such resource file: " + file);
314                         }
315
316                         Reader reader = new InputStreamReader(stream, "UTF-8");
317                         StringBuilder out = new StringBuilder();
318                         char[] buf = new char[1024];
319                         int n;
320
321                         while ((n = reader.read(buf)) >= 0) {
322                                 out.append(buf, 0, n);
323                         }
324
325                         stream.close();
326                         stream = null;
327                         return out.toString();
328                 } finally {
329                         if (stream != null) {
330                                 try {
331                                         stream.close();
332                                 } catch (Exception e) {
333                                         LOGGER.debug("Exception at readResourceFile close stream: " + e);
334                                 }
335                         }
336                 }
337         }
338         
339         /**
340          * Parses the XML document String for the first occurrence of the specified element tag.
341          * If found, the value associated with that element tag is replaced with the new value
342          * and a String containing the modified XML document is returned. If the XML passed is
343          * null or the element tag is not found in the document, null will be returned.
344          * @param xml String containing the original XML document.
345          * @param elementTag String containing the tag of the element to be modified.
346          * @param newValue String containing the new value to be used to modify the corresponding element.
347          * @return the contents of the modified XML document as a String or null/empty if the modification failed.
348          * @throws IOException, TransformerException, ParserConfigurationException, SAXException
349          */
350         public static Optional<String> modifyElement(String xml, String elementTag, String newValue) throws IOException, TransformerException,
351         ParserConfigurationException, SAXException {
352
353                 if (xml == null || xml.isEmpty()) {
354                         // no XML content to be modified, return empty
355                         return Optional.empty();
356                 }
357                 
358                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
359                 dbFactory.setNamespaceAware(true);
360                 DocumentBuilder db = dbFactory.newDocumentBuilder();
361                 InputSource source = new InputSource(new StringReader(xml));
362                 Document doc = db.parse(source);
363                 
364                 Node modNode = doc.getElementsByTagName(elementTag).item(0);
365                 if (modNode == null) {
366                         // did not find the specified element to be modified, return empty
367                         //System.out.println("Did not find element tag " + elementTag + " in XML");
368                         return Optional.empty();
369                 } else {
370                         modNode.setTextContent(newValue);                       
371                 }
372                 
373                 TransformerFactory transformerFactory = TransformerFactory.newInstance();
374                 Transformer transformer = transformerFactory.newTransformer();
375                 StringWriter writer = new StringWriter();
376                 transformer.transform(new DOMSource(doc), new StreamResult(writer));
377                 // return the modified String representation of the XML
378                 return Optional.of(writer.toString().trim());
379         }
380
381         /**
382          * Instantiation is not allowed.
383          */
384         private XmlTool() {
385         }
386 }