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.servlet.result;
018
019import java.io.UnsupportedEncodingException;
020
021import com.jayway.jsonpath.JsonPath;
022import org.hamcrest.Matcher;
023import org.hamcrest.MatcherAssert;
024import org.hamcrest.core.StringStartsWith;
025
026import org.springframework.test.util.JsonPathExpectationsHelper;
027import org.springframework.test.web.servlet.MvcResult;
028import org.springframework.test.web.servlet.ResultMatcher;
029import org.springframework.util.StringUtils;
030
031/**
032 * Factory for assertions on the response content using
033 * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expressions.
034 *
035 * <p>An instance of this class is typically accessed via
036 * {@link MockMvcResultMatchers#jsonPath(String, Matcher)} or
037 * {@link MockMvcResultMatchers#jsonPath(String, Object...)}.
038 *
039 * @author Rossen Stoyanchev
040 * @author Craig Andrews
041 * @author Sam Brannen
042 * @author Brian Clozel
043 * @since 3.2
044 */
045public class JsonPathResultMatchers {
046
047        private final JsonPathExpectationsHelper jsonPathHelper;
048
049        private String prefix;
050
051
052        /**
053         * Protected constructor.
054         * <p>Use {@link MockMvcResultMatchers#jsonPath(String, Object...)} or
055         * {@link MockMvcResultMatchers#jsonPath(String, Matcher)}.
056         * @param expression the {@link JsonPath} expression; never {@code null} or empty
057         * @param args arguments to parameterize the {@code JsonPath} expression with,
058         * using formatting specifiers defined in {@link String#format(String, Object...)}
059         */
060        protected JsonPathResultMatchers(String expression, Object... args) {
061                this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args);
062        }
063
064        /**
065         * Configures the current {@code JsonPathResultMatchers} instance
066         * to verify that the JSON payload is prepended with the given prefix.
067         * <p>Use this method if the JSON payloads are prefixed to avoid
068         * Cross Site Script Inclusion (XSSI) attacks.
069         * @param prefix the string prefix prepended to the actual JSON payload
070         * @since 4.3
071         */
072        public JsonPathResultMatchers prefix(String prefix) {
073                this.prefix = prefix;
074                return this;
075        }
076
077
078        /**
079         * Evaluate the JSON path expression against the response content and
080         * assert the resulting value with the given Hamcrest {@link Matcher}.
081         * @see #value(Matcher, Class)
082         * @see #value(Object)
083         */
084        public <T> ResultMatcher value(final Matcher<T> matcher) {
085                return new ResultMatcher() {
086                        @Override
087                        public void match(MvcResult result) throws Exception {
088                                String content = getContent(result);
089                                jsonPathHelper.assertValue(content, matcher);
090                        }
091                };
092        }
093
094        /**
095         * An overloaded variant of {@link #value(Matcher)} that also accepts a
096         * target type for the resulting value that the matcher can work reliably
097         * against.
098         * <p>This can be useful for matching numbers reliably &mdash; for example,
099         * to coerce an integer into a double.
100         * @since 4.3.15
101         * @see #value(Matcher)
102         * @see #value(Object)
103         */
104        public <T> ResultMatcher value(final Matcher<T> matcher, final Class<T> targetType) {
105                return new ResultMatcher() {
106                        @Override
107                        public void match(MvcResult result) throws Exception {
108                                String content = getContent(result);
109                                jsonPathHelper.assertValue(content, matcher, targetType);
110                        }
111                };
112        }
113
114        /**
115         * Evaluate the JSON path expression against the response content and
116         * assert that the result is equal to the supplied value.
117         * @see #value(Matcher)
118         * @see #value(Matcher, Class)
119         */
120        public ResultMatcher value(final Object expectedValue) {
121                return new ResultMatcher() {
122                        @Override
123                        public void match(MvcResult result) throws Exception {
124                                jsonPathHelper.assertValue(getContent(result), expectedValue);
125                        }
126                };
127        }
128
129        /**
130         * Evaluate the JSON path expression against the response content and
131         * assert that a non-null value exists at the given path.
132         * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite
133         * definite}, this method asserts that the value at the given path is not
134         * <em>empty</em>.
135         */
136        public ResultMatcher exists() {
137                return new ResultMatcher() {
138                        @Override
139                        public void match(MvcResult result) throws Exception {
140                                String content = getContent(result);
141                                jsonPathHelper.exists(content);
142                        }
143                };
144        }
145
146        /**
147         * Evaluate the JSON path expression against the response content and
148         * assert that a value does not exist at the given path.
149         * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite
150         * definite}, this method asserts that the value at the given path is
151         * <em>empty</em>.
152         */
153        public ResultMatcher doesNotExist() {
154                return new ResultMatcher() {
155                        @Override
156                        public void match(MvcResult result) throws Exception {
157                                String content = getContent(result);
158                                jsonPathHelper.doesNotExist(content);
159                        }
160                };
161        }
162
163        /**
164         * Evaluate the JSON path expression against the response content and
165         * assert that an empty value exists at the given path.
166         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
167         * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}.
168         * @since 4.2.1
169         * @see #isNotEmpty()
170         * @see #exists()
171         * @see #doesNotExist()
172         */
173        public ResultMatcher isEmpty() {
174                return new ResultMatcher() {
175                        @Override
176                        public void match(MvcResult result) throws Exception {
177                                String content = getContent(result);
178                                jsonPathHelper.assertValueIsEmpty(content);
179                        }
180                };
181        }
182
183        /**
184         * Evaluate the JSON path expression against the response content and
185         * assert that a non-empty value exists at the given path.
186         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
187         * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}.
188         * @since 4.2.1
189         * @see #isEmpty()
190         * @see #exists()
191         * @see #doesNotExist()
192         */
193        public ResultMatcher isNotEmpty() {
194                return new ResultMatcher() {
195                        @Override
196                        public void match(MvcResult result) throws Exception {
197                                String content = getContent(result);
198                                jsonPathHelper.assertValueIsNotEmpty(content);
199                        }
200                };
201        }
202
203        /**
204         * Evaluate the JSON path expression against the response content and
205         * assert that the result is a {@link String}.
206         * @since 4.2.1
207         */
208        public ResultMatcher isString() {
209                return new ResultMatcher() {
210                        @Override
211                        public void match(MvcResult result) throws Exception {
212                                String content = getContent(result);
213                                jsonPathHelper.assertValueIsString(content);
214                        }
215                };
216        }
217
218        /**
219         * Evaluate the JSON path expression against the response content and
220         * assert that the result is a {@link Boolean}.
221         * @since 4.2.1
222         */
223        public ResultMatcher isBoolean() {
224                return new ResultMatcher() {
225                        @Override
226                        public void match(MvcResult result) throws Exception {
227                                String content = getContent(result);
228                                jsonPathHelper.assertValueIsBoolean(content);
229                        }
230                };
231        }
232
233        /**
234         * Evaluate the JSON path expression against the response content and
235         * assert that the result is a {@link Number}.
236         * @since 4.2.1
237         */
238        public ResultMatcher isNumber() {
239                return new ResultMatcher() {
240                        @Override
241                        public void match(MvcResult result) throws Exception {
242                                String content = getContent(result);
243                                jsonPathHelper.assertValueIsNumber(content);
244                        }
245                };
246        }
247
248        /**
249         * Evaluate the JSON path expression against the response content and
250         * assert that the result is an array.
251         */
252        public ResultMatcher isArray() {
253                return new ResultMatcher() {
254                        @Override
255                        public void match(MvcResult result) throws Exception {
256                                String content = getContent(result);
257                                jsonPathHelper.assertValueIsArray(content);
258                        }
259                };
260        }
261
262        /**
263         * Evaluate the JSON path expression against the response content and
264         * assert that the result is a {@link java.util.Map}.
265         * @since 4.2.1
266         */
267        public ResultMatcher isMap() {
268                return new ResultMatcher() {
269                        @Override
270                        public void match(MvcResult result) throws Exception {
271                                String content = getContent(result);
272                                jsonPathHelper.assertValueIsMap(content);
273                        }
274                };
275        }
276
277        private String getContent(MvcResult result) throws UnsupportedEncodingException {
278                String content = result.getResponse().getContentAsString();
279                if (StringUtils.hasLength(this.prefix)) {
280                        try {
281                                String reason = String.format("Expected a JSON payload prefixed with \"%s\" but found: %s",
282                                                this.prefix, StringUtils.quote(content.substring(0, this.prefix.length())));
283                                MatcherAssert.assertThat(reason, content, StringStartsWith.startsWith(this.prefix));
284                                return content.substring(this.prefix.length());
285                        }
286                        catch (StringIndexOutOfBoundsException ex) {
287                                throw new AssertionError("JSON prefix \"" + this.prefix + "\" not found: " + ex);
288                        }
289                }
290                else {
291                        return content;
292                }
293        }
294
295}