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.text.ParseException;
021
022import com.jayway.jsonpath.JsonPath;
023import org.hamcrest.Matcher;
024
025import org.springframework.http.client.ClientHttpRequest;
026import org.springframework.mock.http.client.MockClientHttpRequest;
027import org.springframework.test.util.JsonPathExpectationsHelper;
028import org.springframework.test.web.client.RequestMatcher;
029
030/**
031 * Factory for assertions on the request content using
032 * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expressions.
033 *
034 * <p>An instance of this class is typically accessed via
035 * {@link MockRestRequestMatchers#jsonPath(String, Matcher)} or
036 * {@link MockRestRequestMatchers#jsonPath(String, Object...)}.
037 *
038 * @author Rossen Stoyanchev
039 * @author Sam Brannen
040 * @since 3.2
041 */
042public class JsonPathRequestMatchers {
043
044        private final JsonPathExpectationsHelper jsonPathHelper;
045
046
047        /**
048         * Protected constructor.
049         * <p>Use {@link MockRestRequestMatchers#jsonPath(String, Matcher)} or
050         * {@link MockRestRequestMatchers#jsonPath(String, Object...)}.
051         * @param expression the {@link JsonPath} expression; never {@code null} or empty
052         * @param args arguments to parameterize the {@code JsonPath} expression with,
053         * using formatting specifiers defined in {@link String#format(String, Object...)}
054         */
055        protected JsonPathRequestMatchers(String expression, Object ... args) {
056                this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args);
057        }
058
059
060        /**
061         * Evaluate the JSON path expression against the request content and
062         * assert the resulting value with the given Hamcrest {@link Matcher}.
063         */
064        public <T> RequestMatcher value(final Matcher<T> matcher) {
065                return new AbstractJsonPathRequestMatcher() {
066                        @Override
067                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
068                                JsonPathRequestMatchers.this.jsonPathHelper.assertValue(request.getBodyAsString(), matcher);
069                        }
070                };
071        }
072
073        /**
074         * An overloaded variant of {@link #value(Matcher)} that also accepts a
075         * target type for the resulting value that the matcher can work reliably
076         * against.
077         * <p>This can be useful for matching numbers reliably &mdash; for example,
078         * to coerce an integer into a double.
079         * @since 4.3.3
080         */
081        public <T> RequestMatcher value(final Matcher<T> matcher, final Class<T> targetType) {
082                return new AbstractJsonPathRequestMatcher() {
083                        @Override
084                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
085                                String body = request.getBodyAsString();
086                                JsonPathRequestMatchers.this.jsonPathHelper.assertValue(body, matcher, targetType);
087                        }
088                };
089        }
090
091        /**
092         * Evaluate the JSON path expression against the request content and
093         * assert that the result is equal to the supplied value.
094         */
095        public RequestMatcher value(final Object expectedValue) {
096                return new AbstractJsonPathRequestMatcher() {
097                        @Override
098                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
099                                JsonPathRequestMatchers.this.jsonPathHelper.assertValue(request.getBodyAsString(), expectedValue);
100                        }
101                };
102        }
103
104        /**
105         * Evaluate the JSON path expression against the request content and
106         * assert that a non-null value exists at the given path.
107         * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite
108         * definite}, this method asserts that the value at the given path is not
109         * <em>empty</em>.
110         */
111        public RequestMatcher exists() {
112                return new AbstractJsonPathRequestMatcher() {
113                        @Override
114                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
115                                JsonPathRequestMatchers.this.jsonPathHelper.exists(request.getBodyAsString());
116                        }
117                };
118        }
119
120        /**
121         * Evaluate the JSON path expression against the request content and
122         * assert that a value does not exist at the given path.
123         * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite
124         * definite}, this method asserts that the value at the given path is
125         * <em>empty</em>.
126         */
127        public RequestMatcher doesNotExist() {
128                return new AbstractJsonPathRequestMatcher() {
129                        @Override
130                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
131                                JsonPathRequestMatchers.this.jsonPathHelper.doesNotExist(request.getBodyAsString());
132                        }
133                };
134        }
135
136        /**
137         * Evaluate the JSON path expression against the request content and
138         * assert that an empty value exists at the given path.
139         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
140         * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}.
141         * @since 4.2.1
142         * @see #isNotEmpty()
143         * @see #exists()
144         * @see #doesNotExist()
145         */
146        public RequestMatcher isEmpty() {
147                return new AbstractJsonPathRequestMatcher() {
148                        @Override
149                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
150                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsEmpty(request.getBodyAsString());
151                        }
152                };
153        }
154
155        /**
156         * Evaluate the JSON path expression against the request content and
157         * assert that a non-empty value exists at the given path.
158         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
159         * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}.
160         * @since 4.2.1
161         * @see #isEmpty()
162         * @see #exists()
163         * @see #doesNotExist()
164         */
165        public RequestMatcher isNotEmpty() {
166                return new AbstractJsonPathRequestMatcher() {
167                        @Override
168                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
169                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsNotEmpty(request.getBodyAsString());
170                        }
171                };
172        }
173
174        /**
175         * Evaluate the JSON path expression against the request content and
176         * assert that the result is a {@link String}.
177         * @since 4.2.1
178         */
179        public RequestMatcher isString() {
180                return new AbstractJsonPathRequestMatcher() {
181                        @Override
182                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
183                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsString(request.getBodyAsString());
184                        }
185                };
186        }
187
188        /**
189         * Evaluate the JSON path expression against the request content and
190         * assert that the result is a {@link Boolean}.
191         * @since 4.2.1
192         */
193        public RequestMatcher isBoolean() {
194                return new AbstractJsonPathRequestMatcher() {
195                        @Override
196                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
197                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsBoolean(request.getBodyAsString());
198                        }
199                };
200        }
201
202        /**
203         * Evaluate the JSON path expression against the request content and
204         * assert that the result is a {@link Number}.
205         * @since 4.2.1
206         */
207        public RequestMatcher isNumber() {
208                return new AbstractJsonPathRequestMatcher() {
209                        @Override
210                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
211                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsNumber(request.getBodyAsString());
212                        }
213                };
214        }
215
216        /**
217         * Evaluate the JSON path expression against the request content and
218         * assert that the result is an array.
219         */
220        public RequestMatcher isArray() {
221                return new AbstractJsonPathRequestMatcher() {
222                        @Override
223                        protected void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
224                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsArray(request.getBodyAsString());
225                        }
226                };
227        }
228
229        /**
230         * Evaluate the JSON path expression against the request content and
231         * assert that the result is a {@link java.util.Map}.
232         * @since 4.2.1
233         */
234        public RequestMatcher isMap() {
235                return new AbstractJsonPathRequestMatcher() {
236                        @Override
237                        public void matchInternal(MockClientHttpRequest request) throws IOException, ParseException {
238                                JsonPathRequestMatchers.this.jsonPathHelper.assertValueIsMap(request.getBodyAsString());
239                        }
240                };
241        }
242
243
244        /**
245         * Abstract base class for {@code JsonPath}-based {@link RequestMatcher}s.
246         * @see #matchInternal
247         */
248        private abstract static class AbstractJsonPathRequestMatcher implements RequestMatcher {
249
250                @Override
251                public final void match(ClientHttpRequest request) throws IOException, AssertionError {
252                        try {
253                                MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
254                                matchInternal(mockRequest);
255                        }
256                        catch (ParseException ex) {
257                                throw new AssertionError("Failed to parse JSON request content: " + ex.getMessage());
258                        }
259                }
260
261                abstract void matchInternal(MockClientHttpRequest request) throws IOException, ParseException;
262        }
263
264}