001/*
002 * Copyright 2002-2020 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.util.List;
020import java.util.Map;
021
022import com.jayway.jsonpath.JsonPath;
023import org.hamcrest.CoreMatchers;
024import org.hamcrest.Matcher;
025import org.hamcrest.MatcherAssert;
026
027import org.springframework.lang.Nullable;
028import org.springframework.util.Assert;
029import org.springframework.util.ClassUtils;
030import org.springframework.util.ObjectUtils;
031import org.springframework.util.StringUtils;
032
033/**
034 * A helper class for applying assertions via JSON path expressions.
035 *
036 * <p>Based on the <a href="https://github.com/jayway/JsonPath">JsonPath</a>
037 * project: requiring version 0.9+, with 1.1+ strongly recommended.
038 *
039 * @author Rossen Stoyanchev
040 * @author Juergen Hoeller
041 * @author Craig Andrews
042 * @author Sam Brannen
043 * @since 3.2
044 */
045public class JsonPathExpectationsHelper {
046
047        private final String expression;
048
049        private final JsonPath jsonPath;
050
051
052        /**
053         * Construct a new {@code JsonPathExpectationsHelper}.
054         * @param expression the {@link JsonPath} expression; never {@code null} or empty
055         * @param args arguments to parameterize the {@code JsonPath} expression with,
056         * using formatting specifiers defined in {@link String#format(String, Object...)}
057         */
058        public JsonPathExpectationsHelper(String expression, Object... args) {
059                Assert.hasText(expression, "expression must not be null or empty");
060                this.expression = String.format(expression, args);
061                this.jsonPath = JsonPath.compile(this.expression);
062        }
063
064
065        /**
066         * Evaluate the JSON path expression against the supplied {@code content}
067         * and assert the resulting value with the given {@code Matcher}.
068         * @param content the JSON content
069         * @param matcher the matcher with which to assert the result
070         */
071        @SuppressWarnings("unchecked")
072        public <T> void assertValue(String content, Matcher<T> matcher) {
073                T value = (T) evaluateJsonPath(content);
074                MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher);
075        }
076
077        /**
078         * An overloaded variant of {@link #assertValue(String, Matcher)} that also
079         * accepts a target type for the resulting value. This can be useful for
080         * matching numbers reliably for example coercing an integer into a double.
081         * @param content the JSON content
082         * @param matcher the matcher with which to assert the result
083         * @param targetType the expected type of the resulting value
084         * @since 4.3.3
085         */
086        @SuppressWarnings("unchecked")
087        public <T> void assertValue(String content, Matcher<T> matcher, Class<T> targetType) {
088                T value = (T) evaluateJsonPath(content, targetType);
089                MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher);
090        }
091
092        /**
093         * Evaluate the JSON path expression against the supplied {@code content}
094         * and assert that the result is equal to the expected value.
095         * @param content the JSON content
096         * @param expectedValue the expected value
097         */
098        public void assertValue(String content, @Nullable Object expectedValue) {
099                Object actualValue = evaluateJsonPath(content);
100                if ((actualValue instanceof List) && !(expectedValue instanceof List)) {
101                        @SuppressWarnings("rawtypes")
102                        List actualValueList = (List) actualValue;
103                        if (actualValueList.isEmpty()) {
104                                AssertionErrors.fail("No matching value at JSON path \"" + this.expression + "\"");
105                        }
106                        if (actualValueList.size() != 1) {
107                                AssertionErrors.fail("Got a list of values " + actualValue +
108                                                " instead of the expected single value " + expectedValue);
109                        }
110                        actualValue = actualValueList.get(0);
111                }
112                else if (actualValue != null && expectedValue != null &&
113                                !actualValue.getClass().equals(expectedValue.getClass())) {
114                        try {
115                                actualValue = evaluateJsonPath(content, expectedValue.getClass());
116                        }
117                        catch (AssertionError error) {
118                                String message = String.format(
119                                        "At JSON path \"%s\", value <%s> of type <%s> cannot be converted to type <%s>",
120                                        this.expression, actualValue, ClassUtils.getDescriptiveType(actualValue),
121                                        ClassUtils.getDescriptiveType(expectedValue));
122                                throw new AssertionError(message, error.getCause());
123                        }
124                }
125                AssertionErrors.assertEquals("JSON path \"" + this.expression + "\"", expectedValue, actualValue);
126        }
127
128        /**
129         * Evaluate the JSON path expression against the supplied {@code content}
130         * and assert that the resulting value is a {@link String}.
131         * @param content the JSON content
132         * @since 4.2.1
133         */
134        public void assertValueIsString(String content) {
135                Object value = assertExistsAndReturn(content);
136                MatcherAssert.assertThat(failureReason("a string", value), value, CoreMatchers.instanceOf(String.class));
137        }
138
139        /**
140         * Evaluate the JSON path expression against the supplied {@code content}
141         * and assert that the resulting value is a {@link Boolean}.
142         * @param content the JSON content
143         * @since 4.2.1
144         */
145        public void assertValueIsBoolean(String content) {
146                Object value = assertExistsAndReturn(content);
147                MatcherAssert.assertThat(failureReason("a boolean", value), value, CoreMatchers.instanceOf(Boolean.class));
148        }
149
150        /**
151         * Evaluate the JSON path expression against the supplied {@code content}
152         * and assert that the resulting value is a {@link Number}.
153         * @param content the JSON content
154         * @since 4.2.1
155         */
156        public void assertValueIsNumber(String content) {
157                Object value = assertExistsAndReturn(content);
158                MatcherAssert.assertThat(failureReason("a number", value), value, CoreMatchers.instanceOf(Number.class));
159        }
160
161        /**
162         * Evaluate the JSON path expression against the supplied {@code content}
163         * and assert that the resulting value is an array.
164         * @param content the JSON content
165         */
166        public void assertValueIsArray(String content) {
167                Object value = assertExistsAndReturn(content);
168                MatcherAssert.assertThat(failureReason("an array", value), value, CoreMatchers.instanceOf(List.class));
169        }
170
171        /**
172         * Evaluate the JSON path expression against the supplied {@code content}
173         * and assert that the resulting value is a {@link Map}.
174         * @param content the JSON content
175         * @since 4.2.1
176         */
177        public void assertValueIsMap(String content) {
178                Object value = assertExistsAndReturn(content);
179                MatcherAssert.assertThat(failureReason("a map", value), value, CoreMatchers.instanceOf(Map.class));
180        }
181
182        /**
183         * Evaluate the JSON path expression against the supplied {@code content}
184         * and assert that a non-null value, possibly an empty array or map, exists
185         * at the given path.
186         * <p>Note that if the JSON path expression is not
187         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
188         * that the list of values at the given path is not <em>empty</em>.
189         * @param content the JSON content
190         */
191        public void exists(String content) {
192                assertExistsAndReturn(content);
193        }
194
195        /**
196         * Evaluate the JSON path expression against the supplied {@code content}
197         * and assert that a non-null value does not exist at the given path.
198         * <p>Note that if the JSON path expression is not
199         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
200         * that the list of values at the given path is <em>empty</em>.
201         * @param content the JSON content
202         */
203        public void doesNotExist(String content) {
204                Object value;
205                try {
206                        value = evaluateJsonPath(content);
207                }
208                catch (AssertionError ex) {
209                        return;
210                }
211                String reason = failureReason("no value", value);
212                if (pathIsIndefinite() && value instanceof List) {
213                        AssertionErrors.assertTrue(reason, ((List<?>) value).isEmpty());
214                }
215                else {
216                        AssertionErrors.assertTrue(reason, (value == null));
217                }
218        }
219
220        /**
221         * Evaluate the JSON path expression against the supplied {@code content}
222         * and assert that an empty value exists at the given path.
223         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
224         * {@link ObjectUtils#isEmpty(Object)}.
225         * @param content the JSON content
226         */
227        public void assertValueIsEmpty(String content) {
228                Object value = evaluateJsonPath(content);
229                AssertionErrors.assertTrue(failureReason("an empty value", value), ObjectUtils.isEmpty(value));
230        }
231
232        /**
233         * Evaluate the JSON path expression against the supplied {@code content}
234         * and assert that a non-empty value exists at the given path.
235         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
236         * {@link ObjectUtils#isEmpty(Object)}.
237         * @param content the JSON content
238         */
239        public void assertValueIsNotEmpty(String content) {
240                Object value = evaluateJsonPath(content);
241                AssertionErrors.assertTrue(failureReason("a non-empty value", value), !ObjectUtils.isEmpty(value));
242        }
243
244        /**
245         * Evaluate the JSON path expression against the supplied {@code content}
246         * and assert that a value, possibly {@code null}, exists.
247         * <p>If the JSON path expression is not
248         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
249         * that the list of values at the given path is not <em>empty</em>.
250         * @param content the JSON content
251         * @since 5.0.3
252         */
253        public void hasJsonPath(String content) {
254                Object value = evaluateJsonPath(content);
255                if (pathIsIndefinite() && value instanceof List) {
256                        String message = "No values for JSON path \"" + this.expression + "\"";
257                        AssertionErrors.assertTrue(message, !((List<?>) value).isEmpty());
258                }
259        }
260
261        /**
262         * Evaluate the JSON path expression against the supplied {@code content}
263         * and assert that a value, including {@code null} values, does not exist
264         * at the given path.
265         * <p>If the JSON path expression is not
266         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
267         * that the list of values at the given path is <em>empty</em>.
268         * @param content the JSON content
269         * @since 5.0.3
270         */
271        public void doesNotHaveJsonPath(String content) {
272                Object value;
273                try {
274                        value = evaluateJsonPath(content);
275                }
276                catch (AssertionError ex) {
277                        return;
278                }
279                if (pathIsIndefinite() && value instanceof List) {
280                        AssertionErrors.assertTrue(failureReason("no values", value), ((List<?>) value).isEmpty());
281                }
282                else {
283                        AssertionErrors.fail(failureReason("no value", value));
284                }
285        }
286
287        private String failureReason(String expectedDescription, @Nullable Object value) {
288                return String.format("Expected %s at JSON path \"%s\" but found: %s", expectedDescription, this.expression,
289                                ObjectUtils.nullSafeToString(StringUtils.quoteIfString(value)));
290        }
291
292        /**
293         * Evaluate the JSON path and return the resulting value.
294         * @param content the content to evaluate against
295         * @return the result of the evaluation
296         * @throws AssertionError if the evaluation fails
297         */
298        @Nullable
299        public Object evaluateJsonPath(String content) {
300                try {
301                        return this.jsonPath.read(content);
302                }
303                catch (Throwable ex) {
304                        throw new AssertionError("No value at JSON path \"" + this.expression + "\"", ex);
305                }
306        }
307
308        /**
309         * Variant of {@link #evaluateJsonPath(String)} with a target type.
310         * <p>This can be useful for matching numbers reliably for example coercing an
311         * integer into a double.
312         * @param content the content to evaluate against
313         * @return the result of the evaluation
314         * @throws AssertionError if the evaluation fails
315         */
316        public Object evaluateJsonPath(String content, Class<?> targetType) {
317                try {
318                        return JsonPath.parse(content).read(this.expression, targetType);
319                }
320                catch (Throwable ex) {
321                        String message = "No value at JSON path \"" + this.expression + "\"";
322                        throw new AssertionError(message, ex);
323                }
324        }
325
326        @Nullable
327        private Object assertExistsAndReturn(String content) {
328                Object value = evaluateJsonPath(content);
329                String reason = "No value at JSON path \"" + this.expression + "\"";
330                AssertionErrors.assertTrue(reason, value != null);
331                if (pathIsIndefinite() && value instanceof List) {
332                        AssertionErrors.assertTrue(reason, !((List<?>) value).isEmpty());
333                }
334                return value;
335        }
336
337        private boolean pathIsIndefinite() {
338                return !this.jsonPath.isDefinite();
339        }
340
341}