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.web.client.match;
018
019import java.io.IOException;
020import java.util.Map;
021import javax.xml.xpath.XPathExpressionException;
022
023import org.hamcrest.Matcher;
024import org.w3c.dom.Node;
025
026import org.springframework.http.client.ClientHttpRequest;
027import org.springframework.mock.http.client.MockClientHttpRequest;
028import org.springframework.test.util.XpathExpectationsHelper;
029import org.springframework.test.web.client.RequestMatcher;
030
031/**
032 * Factory methods for request content {@code RequestMatcher}'s using an XPath
033 * expression. An instance of this class is typically accessed via
034 * {@code RequestMatchers.xpath(..)}.
035 *
036 * @author Rossen Stoyanchev
037 * @since 3.2
038 */
039public class XpathRequestMatchers {
040
041        private static final String DEFAULT_ENCODING = "UTF-8";
042
043        private final XpathExpectationsHelper xpathHelper;
044
045
046        /**
047         * Class constructor, not for direct instantiation. Use
048         * {@link MockRestRequestMatchers#xpath(String, Object...)} or
049         * {@link MockRestRequestMatchers#xpath(String, Map, Object...)}.
050         * @param expression the XPath expression
051         * @param namespaces XML namespaces referenced in the XPath expression, or {@code null}
052         * @param args arguments to parameterize the XPath expression with using the
053         * formatting specifiers defined in {@link String#format(String, Object...)}
054         * @throws XPathExpressionException if expression compilation failed
055         */
056        protected XpathRequestMatchers(String expression, Map<String, String> namespaces, Object ... args)
057                        throws XPathExpressionException {
058
059                this.xpathHelper = new XpathExpectationsHelper(expression, namespaces, args);
060        }
061
062
063        /**
064         * Apply the XPath and assert it with the given {@code Matcher<Node>}.
065         */
066        public <T> RequestMatcher node(final Matcher<? super Node> matcher) {
067                return new AbstractXpathRequestMatcher() {
068                        @Override
069                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
070                                xpathHelper.assertNode(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
071                        }
072                };
073        }
074
075        /**
076         * Assert that content exists at the given XPath.
077         */
078        public <T> RequestMatcher exists() {
079                return new AbstractXpathRequestMatcher() {
080                        @Override
081                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
082                                xpathHelper.exists(request.getBodyAsBytes(), DEFAULT_ENCODING);
083                        }
084                };
085        }
086
087        /**
088         * Assert that content does not exist at the given XPath.
089         */
090        public <T> RequestMatcher doesNotExist() {
091                return new AbstractXpathRequestMatcher() {
092                        @Override
093                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
094                                xpathHelper.doesNotExist(request.getBodyAsBytes(), DEFAULT_ENCODING);
095                        }
096                };
097        }
098
099        /**
100         * Apply the XPath and assert the number of nodes found with the given
101         * {@code Matcher<Integer>}.
102         */
103        public <T> RequestMatcher nodeCount(final Matcher<Integer> matcher) {
104                return new AbstractXpathRequestMatcher() {
105                        @Override
106                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
107                                xpathHelper.assertNodeCount(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
108                        }
109                };
110        }
111
112        /**
113         * Apply the XPath and assert the number of nodes found.
114         */
115        public <T> RequestMatcher nodeCount(final int expectedCount) {
116                return new AbstractXpathRequestMatcher() {
117                        @Override
118                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
119                                xpathHelper.assertNodeCount(request.getBodyAsBytes(), DEFAULT_ENCODING, expectedCount);
120                        }
121                };
122        }
123
124        /**
125         * Apply the XPath and assert the String content found with the given matcher.
126         */
127        public <T> RequestMatcher string(final Matcher<? super String> matcher) {
128                return new AbstractXpathRequestMatcher() {
129                        @Override
130                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
131                                xpathHelper.assertString(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
132                        }
133                };
134        }
135
136        /**
137         * Apply the XPath and assert the String content found.
138         */
139        public RequestMatcher string(final String value) {
140                return new AbstractXpathRequestMatcher() {
141                        @Override
142                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
143                                xpathHelper.assertString(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
144                        }
145                };
146        }
147
148        /**
149         * Apply the XPath and assert the number found with the given matcher.
150         */
151        public <T> RequestMatcher number(final Matcher<? super Double> matcher) {
152                return new AbstractXpathRequestMatcher() {
153                        @Override
154                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
155                                xpathHelper.assertNumber(request.getBodyAsBytes(), DEFAULT_ENCODING, matcher);
156                        }
157                };
158        }
159
160        /**
161         * Apply the XPath and assert the number of nodes found.
162         */
163        public RequestMatcher number(final Double value) {
164                return new AbstractXpathRequestMatcher() {
165                        @Override
166                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
167                                xpathHelper.assertNumber(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
168                        }
169                };
170        }
171
172        /**
173         * Apply the XPath and assert the boolean value found.
174         */
175        public <T> RequestMatcher booleanValue(final Boolean value) {
176                return new AbstractXpathRequestMatcher() {
177                        @Override
178                        protected void matchInternal(MockClientHttpRequest request) throws Exception {
179                                xpathHelper.assertBoolean(request.getBodyAsBytes(), DEFAULT_ENCODING, value);
180                        }
181                };
182        }
183
184
185        /**
186         * Abstract base class for XPath {@link RequestMatcher}'s.
187         */
188        private abstract static class AbstractXpathRequestMatcher implements RequestMatcher {
189
190                @Override
191                public final void match(ClientHttpRequest request) throws IOException, AssertionError {
192                        try {
193                                MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
194                                matchInternal(mockRequest);
195                        }
196                        catch (Exception ex) {
197                                throw new AssertionError("Failed to parse XML request content: " + ex.getMessage());
198                        }
199                }
200
201                protected abstract void matchInternal(MockClientHttpRequest request) throws Exception;
202        }
203
204}