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 }