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