View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat.testhelpers;
20  
21  import static org.assertj.core.api.Assertions.assertThat;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.io.OutputStreamWriter;
28  import java.io.PrintStream;
29  import java.io.StringReader;
30  import java.lang.reflect.UndeclaredThrowableException;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.List;
35  
36  import java.util.Map;
37  import javax.xml.namespace.QName;
38  import javax.xml.parsers.ParserConfigurationException;
39  import javax.xml.transform.OutputKeys;
40  import javax.xml.transform.Transformer;
41  import javax.xml.transform.TransformerException;
42  import javax.xml.transform.TransformerFactory;
43  import javax.xml.transform.dom.DOMSource;
44  import javax.xml.transform.stream.StreamResult;
45  import javax.xml.xpath.XPath;
46  import javax.xml.xpath.XPathConstants;
47  import javax.xml.xpath.XPathExpression;
48  import javax.xml.xpath.XPathExpressionException;
49  
50  import org.apache.commons.xml.secure.SecureSAXParserFactory;
51  import org.apache.commons.xml.secure.SecureTransformerFactory;
52  import org.apache.rat.report.xml.writer.XmlWriter;
53  import org.apache.rat.utils.DefaultLog;
54  import org.apache.rat.utils.StandardXmlFactory;
55  import org.opentest4j.AssertionFailedError;
56  import org.w3c.dom.Document;
57  import org.w3c.dom.NamedNodeMap;
58  import org.w3c.dom.Node;
59  import org.w3c.dom.NodeList;
60  import org.xml.sax.InputSource;
61  import org.xml.sax.SAXException;
62  import org.xml.sax.XMLReader;
63  
64  /**
65   * Utilities to help test XML documents.
66   */
67  public final class XmlUtils {
68      /**
69       * Private constructor, to prevent accidental instantiation.
70       */
71      private XmlUtils() {
72          // Does nothing
73      }
74  
75      /**
76       * Construct a safe XML reader.
77       * @return an XML reader.
78       * @throws SAXException on SAX exception.
79       * @throws ParserConfigurationException on parser configuration exception.
80       */
81      public static XMLReader newXMLReader() throws SAXException, ParserConfigurationException {
82          return SecureSAXParserFactory.newNSInstance().newSAXParser().getXMLReader();
83      }
84  
85      /**
86       * Determines whether the document string is well-formed.
87       * @param string the document string to check.
88       * @return {@code true} if the document is well-formed, {@code false} otherwise.
89       */
90      public static boolean isWellFormedXml(final String string) {
91          return isWellFormedXml(new InputSource(new StringReader(string)));
92      }
93  
94      /**
95       * Determines whether the document in the input stream is well-formed.
96       * @param in the input stream containing the document.
97       * @return {@code true} if the document is well-formed, {@code false} otherwise.
98       */
99      public static boolean isWellFormedXml(final InputStream in) {
100         return isWellFormedXml(new InputSource(in));
101     }
102 
103     /**
104      * Determines whether the document in the input source is well-formed.
105      * @param isource the input source containing the document.
106      * @return {@code true} if the document is well-formed, {@code false} otherwise.
107      */
108     public static boolean isWellFormedXml(final InputSource isource) {
109         try {
110             newXMLReader().parse(isource);
111             return true;
112         } catch (SAXException e) {
113             DefaultLog.getInstance().error(e.getMessage(), e);
114             return false;
115         } catch (IOException | ParserConfigurationException e) {
116             throw new UndeclaredThrowableException(e);
117         }
118     }
119 
120     /**
121      * Gets a Nodelist from an XPath string.
122      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
123      * @param xPath the XPath object to compile the statement with.
124      * @param xpath the XPath statement to compile.
125      * @return the NodeList of nodes that matches the XPath.
126      * @throws XPathExpressionException on error.
127      */
128     public static NodeList getNodeList(Object source, XPath xPath, String xpath) throws XPathExpressionException {
129         return (NodeList) xPath.compile(xpath).evaluate(source, XPathConstants.NODESET);
130     }
131 
132     /**
133      * Determines whether an XPath identified a node in the source.
134      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
135      * @param xPath the XPath object to compile the statement with.
136      * @param xpath the XPath statement to compile.
137      * @return {@code true} if the document contains the node identified by the XPath statement, {@code false} otherwise.
138      * @throws XPathExpressionException on error.
139      */
140     public static boolean isPresent(Object source, XPath xPath, String xpath) throws XPathExpressionException {
141         Object node = xPath.compile(xpath).evaluate(source, XPathConstants.NODE);
142         return node != null;
143     }
144 
145     /**
146      * Gets a List of Nodes from an XPath string.
147      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
148      * @param xPath the XPath object to compile the statement with.
149      * @param xpath the XPath statement to compile.
150      * @return the list of nodes that match the XPath.
151      * @throws XPathExpressionException on error.
152      */
153     public static List<Node> getNodes(Object source, XPath xPath, String xpath) throws XPathExpressionException {
154         NodeList nodeList = (NodeList) xPath.compile(xpath).evaluate(source, XPathConstants.NODESET);
155         List<Node> result = new ArrayList<>();
156         for (int i = 0; i < nodeList.getLength(); i++) {
157             result.add(nodeList.item(i));
158         }
159         return result;
160     }
161 
162     /**
163      * Gets a node identified by an XPath.
164      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
165      * @param xPath the XPath object to compile the statement with.
166      * @param xpath the XPath statement to compile.
167      * @return the identified Node.
168      * @throws XPathExpressionException on error.
169      * @throws AssertionFailedError if more than one node is found.
170      */
171     public static Node getNode(Object source, XPath xPath, String xpath) throws XPathExpressionException {
172         NodeList nodeList = getNodeList(source, xPath, xpath);
173         assertEquals(1, nodeList.getLength(), "Could not find exactly one " + xpath);
174         return nodeList.item(0);
175     }
176 
177     /**
178      * Prints the specified NodeList as a string representation of its contents.
179      * @param nodeList the Nodelist to print.
180      * @return the string that contains the textual representation of the NodeList nodes.
181      */
182     public static String printNodeList(NodeList nodeList) {
183         StringBuilder sb = new StringBuilder();
184         for (int i = 0; i < nodeList.getLength(); i++) {
185             Node n = nodeList.item(0);
186             sb.append(n.getNodeName()).append(" ");
187             NamedNodeMap nnm = n.getAttributes();
188             for (int j = 0; j < nnm.getLength(); j++) {
189                 Node a = nnm.item(j);
190                 sb.append(a.getNodeName()).append("=").append(a.getNodeValue()).append(" ");
191             }
192             sb.append("\n");
193         }
194         return sb.toString();
195     }
196 
197     /**
198      * Reads an input stream into a document.
199      * @param inputStream the input stream to read.
200      * @return the Document.
201      * @throws SAXException on sax Error
202      * @throws IOException on IO Error
203      */
204     public static Document toDom(final InputStream inputStream)
205             throws SAXException, IOException {
206         return StandardXmlFactory.documentBuilder().parse(inputStream);
207     }
208 
209     /**
210      * Writes a boolean attribute to an XML writer.
211      * @param writer the writer to write to.
212      * @param name the name of the attribute.
213      * @param booleanValue the boolean value.
214      * @throws IOException on write error.
215      */
216     public static void writeAttribute(final XmlWriter writer, final String name, final boolean booleanValue)
217             throws IOException {
218         final String value = Boolean.toString(booleanValue);
219         writer.attribute(name, value);
220     }
221 
222     /**
223      * Print the XML document to the output stream.
224      *
225      * @param out the OutputStream to print the document to.
226      * @param document The XML DOM document to print.
227      */
228     public static void printDocument(OutputStream out, Document document) {
229         TransformerFactory tf = SecureTransformerFactory.newInstance();
230         Transformer transformer;
231         try {
232             transformer = tf.newTransformer();
233 
234             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
235             transformer.setOutputProperty(OutputKeys.METHOD, "xml");
236             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
237             transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
238             transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
239 
240             transformer.transform(new DOMSource(document), new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8)));
241         } catch (TransformerException e) {
242             e.printStackTrace(new PrintStream(out));
243         }
244     }
245 
246     /**
247      * Get an attribute from an XPath statement
248      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
249      * @param xPath the XPath object to compile the statement with.
250      * @param xpath the XPath statement to compile.
251      * @param attribute attribute to retrieve from the node specified by the XPath statement.
252      * @return the string value of the attribute.
253      * @throws XPathExpressionException on error
254      */
255     public static String getAttribute(Object source, XPath xPath, String xpath, String attribute) throws XPathExpressionException {
256         Node node = XmlUtils.getNode(source, xPath, xpath);
257         NamedNodeMap attr = node.getAttributes();
258         node = attr.getNamedItem(attribute);
259         assertThat(node).as(attribute + " was not found").isNotNull();
260         return node.getNodeValue();
261     }
262 
263     public static Map<String, String> mapOf(String... parts) {
264         Map<String, String> map = new HashMap<>();
265         for (int i = 0; i < parts.length; i += 2) {
266             map.put(parts[i], parts[i+1]);
267         }
268         return map;
269     }
270 
271     /**
272      * Assert that attributes are set on a node.
273      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
274      * @param xPath the XPath object to compile the statement with.
275      * @param xpath the XPath statement to compile.
276      * @param attributes a map of attribute names to values.
277      * @throws XPathExpressionException on error.
278      */
279     public static void assertAttributes(Object source, XPath xPath, String xpath, Map<String, String> attributes) throws XPathExpressionException {
280         Node node = XmlUtils.getNode(source, xPath, xpath);
281         NamedNodeMap attr = node.getAttributes();
282         for (Map.Entry<String, String> entry : attributes.entrySet()) {
283             node = attr.getNamedItem(entry.getKey());
284             assertThat(node).as(() -> entry.getKey() + " was not found on " + xpath).isNotNull();
285             assertThat(node.getNodeValue()).as(() -> entry.getKey() + " on " + xpath).isEqualTo(entry.getValue());
286         }
287     }
288 
289     /**
290      * Assert that an XPath is present in the document.
291      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
292      * @param xPath the XPath object to compile the statement with.
293      * @param xpath the XPath statement to compile.
294     */
295     public static void assertIsPresent(Object source, XPath xPath, String xpath) throws XPathExpressionException {
296         assertThat(isPresent(source, xPath, xpath)).as("Presence of " + xpath).isTrue();
297     }
298 
299     /**
300      * Assert that an XPath is not present in the document.
301      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
302      * @param xPath the XPath object to compile the statement with.
303      * @param xpath the XPath statement to compile.
304      */
305     public static void assertIsNotPresent(Object source, XPath xPath, String xpath) throws XPathExpressionException {
306         assertThat(isPresent(source, xPath, xpath)).as("Non-presence of " + xpath).isFalse();
307     }
308 
309     /**
310      * Assert that a named XPath is present in the document.
311      * @param identifier the name of the object.
312      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
313      * @param xPath The XPath object to compile the statement with.
314      * @param xpath the XPath statement to compile.
315      */
316     public static void assertIsPresent(String identifier, Object source, XPath xPath, String xpath) throws XPathExpressionException {
317         assertThat(isPresent(source, xPath, xpath)).as(identifier + ": Presence of " + xpath).isTrue();
318     }
319 
320     /**
321      * Assert that a named XPath is not present in the document.
322      * @param identifier the name of the object.
323      * @param source the context for the XPath statement to be evaluated in. See {@link XPathExpression#evaluate(Object, QName)}.
324      * @param xPath The XPath object to compile the statement with.
325      * @param xpath the XPath statement to compile.
326      */
327     public static void assertIsNotPresent(String identifier, Object source, XPath xPath, String xpath) throws XPathExpressionException {
328         assertThat(isPresent(source, xPath, xpath)).as(identifier + ": Non-presence of " + xpath).isFalse();
329     }
330 }