001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.test.util; 018 019import java.io.ByteArrayInputStream; 020import java.util.Collections; 021import java.util.Map; 022import javax.xml.namespace.QName; 023import javax.xml.parsers.DocumentBuilder; 024import javax.xml.parsers.DocumentBuilderFactory; 025import javax.xml.xpath.XPath; 026import javax.xml.xpath.XPathConstants; 027import javax.xml.xpath.XPathExpression; 028import javax.xml.xpath.XPathExpressionException; 029import javax.xml.xpath.XPathFactory; 030 031import org.hamcrest.Matcher; 032import org.w3c.dom.Document; 033import org.w3c.dom.Node; 034import org.w3c.dom.NodeList; 035import org.xml.sax.InputSource; 036 037import org.springframework.util.CollectionUtils; 038import org.springframework.util.StringUtils; 039import org.springframework.util.xml.SimpleNamespaceContext; 040 041import static org.hamcrest.MatcherAssert.*; 042import static org.springframework.test.util.AssertionErrors.*; 043 044/** 045 * A helper class for applying assertions via XPath expressions. 046 * 047 * @author Rossen Stoyanchev 048 * @since 3.2 049 */ 050public class XpathExpectationsHelper { 051 052 private final String expression; 053 054 private final XPathExpression xpathExpression; 055 056 private final boolean hasNamespaces; 057 058 059 /** 060 * XpathExpectationsHelper constructor. 061 * @param expression the XPath expression 062 * @param namespaces XML namespaces referenced in the XPath expression, or {@code null} 063 * @param args arguments to parameterize the XPath expression with using the 064 * formatting specifiers defined in {@link String#format(String, Object...)} 065 * @throws XPathExpressionException if expression compilation failed 066 */ 067 public XpathExpectationsHelper(String expression, Map<String, String> namespaces, Object... args) 068 throws XPathExpressionException { 069 070 this.expression = String.format(expression, args); 071 this.xpathExpression = compileXpathExpression(this.expression, namespaces); 072 this.hasNamespaces = !CollectionUtils.isEmpty(namespaces); 073 } 074 075 076 private XPathExpression compileXpathExpression(String expression, Map<String, String> namespaces) 077 throws XPathExpressionException { 078 079 SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext(); 080 namespaceContext.setBindings(namespaces != null ? namespaces : Collections.<String, String> emptyMap()); 081 XPath xpath = XPathFactory.newInstance().newXPath(); 082 xpath.setNamespaceContext(namespaceContext); 083 return xpath.compile(expression); 084 } 085 086 /** 087 * Return the compiled XPath expression. 088 */ 089 protected XPathExpression getXpathExpression() { 090 return this.xpathExpression; 091 } 092 093 /** 094 * Parse the content, evaluate the XPath expression as a {@link Node}, 095 * and assert it with the given {@code Matcher<Node>}. 096 */ 097 public void assertNode(byte[] content, String encoding, final Matcher<? super Node> matcher) throws Exception { 098 Document document = parseXmlByteArray(content, encoding); 099 Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); 100 assertThat("XPath " + this.expression, node, matcher); 101 } 102 103 /** 104 * Parse the given XML content to a {@link Document}. 105 * @param xml the content to parse 106 * @param encoding optional content encoding, if provided as metadata (e.g. in HTTP headers) 107 * @return the parsed document 108 */ 109 protected Document parseXmlByteArray(byte[] xml, String encoding) throws Exception { 110 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 111 factory.setNamespaceAware(this.hasNamespaces); 112 DocumentBuilder documentBuilder = factory.newDocumentBuilder(); 113 InputSource inputSource = new InputSource(new ByteArrayInputStream(xml)); 114 if (StringUtils.hasText(encoding)) { 115 inputSource.setEncoding(encoding); 116 } 117 return documentBuilder.parse(inputSource); 118 } 119 120 /** 121 * Apply the XPath expression to given document. 122 * @throws XPathExpressionException if expression evaluation failed 123 */ 124 @SuppressWarnings("unchecked") 125 protected <T> T evaluateXpath(Document document, QName evaluationType, Class<T> expectedClass) 126 throws XPathExpressionException { 127 128 return (T) getXpathExpression().evaluate(document, evaluationType); 129 } 130 131 /** 132 * Apply the XPath expression and assert the resulting content exists. 133 * @throws Exception if content parsing or expression evaluation fails 134 */ 135 public void exists(byte[] content, String encoding) throws Exception { 136 Document document = parseXmlByteArray(content, encoding); 137 Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); 138 assertTrue("XPath " + this.expression + " does not exist", node != null); 139 } 140 141 /** 142 * Apply the XPath expression and assert the resulting content does not exist. 143 * @throws Exception if content parsing or expression evaluation fails 144 */ 145 public void doesNotExist(byte[] content, String encoding) throws Exception { 146 Document document = parseXmlByteArray(content, encoding); 147 Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); 148 assertTrue("XPath " + this.expression + " exists", node == null); 149 } 150 151 /** 152 * Apply the XPath expression and assert the resulting content with the 153 * given Hamcrest matcher. 154 * @throws Exception if content parsing or expression evaluation fails 155 */ 156 public void assertNodeCount(byte[] content, String encoding, Matcher<Integer> matcher) throws Exception { 157 Document document = parseXmlByteArray(content, encoding); 158 NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class); 159 assertThat("nodeCount for XPath " + this.expression, nodeList.getLength(), matcher); 160 } 161 162 /** 163 * Apply the XPath expression and assert the resulting content as an integer. 164 * @throws Exception if content parsing or expression evaluation fails 165 */ 166 public void assertNodeCount(byte[] content, String encoding, int expectedCount) throws Exception { 167 Document document = parseXmlByteArray(content, encoding); 168 NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class); 169 assertEquals("nodeCount for XPath " + this.expression, expectedCount, nodeList.getLength()); 170 } 171 172 /** 173 * Apply the XPath expression and assert the resulting content with the 174 * given Hamcrest matcher. 175 * @throws Exception if content parsing or expression evaluation fails 176 */ 177 public void assertString(byte[] content, String encoding, Matcher<? super String> matcher) throws Exception { 178 Document document = parseXmlByteArray(content, encoding); 179 String result = evaluateXpath(document, XPathConstants.STRING, String.class); 180 assertThat("XPath " + this.expression, result, matcher); 181 } 182 183 /** 184 * Apply the XPath expression and assert the resulting content as a String. 185 * @throws Exception if content parsing or expression evaluation fails 186 */ 187 public void assertString(byte[] content, String encoding, String expectedValue) throws Exception { 188 Document document = parseXmlByteArray(content, encoding); 189 String actual = evaluateXpath(document, XPathConstants.STRING, String.class); 190 assertEquals("XPath " + this.expression, expectedValue, actual); 191 } 192 193 /** 194 * Apply the XPath expression and assert the resulting content with the 195 * given Hamcrest matcher. 196 * @throws Exception if content parsing or expression evaluation fails 197 */ 198 public void assertNumber(byte[] content, String encoding, Matcher<? super Double> matcher) throws Exception { 199 Document document = parseXmlByteArray(content, encoding); 200 Double result = evaluateXpath(document, XPathConstants.NUMBER, Double.class); 201 assertThat("XPath " + this.expression, result, matcher); 202 } 203 204 /** 205 * Apply the XPath expression and assert the resulting content as a Double. 206 * @throws Exception if content parsing or expression evaluation fails 207 */ 208 public void assertNumber(byte[] content, String encoding, Double expectedValue) throws Exception { 209 Document document = parseXmlByteArray(content, encoding); 210 Double actual = evaluateXpath(document, XPathConstants.NUMBER, Double.class); 211 assertEquals("XPath " + this.expression, expectedValue, actual); 212 } 213 214 /** 215 * Apply the XPath expression and assert the resulting content as a Boolean. 216 * @throws Exception if content parsing or expression evaluation fails 217 */ 218 public void assertBoolean(byte[] content, String encoding, boolean expectedValue) throws Exception { 219 Document document = parseXmlByteArray(content, encoding); 220 String actual = evaluateXpath(document, XPathConstants.STRING, String.class); 221 assertEquals("XPath " + this.expression, expectedValue, Boolean.parseBoolean(actual)); 222 } 223 224}