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}