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.util;
018
019import java.util.List;
020import java.util.Map;
021
022import com.jayway.jsonpath.JsonPath;
023import org.hamcrest.Matcher;
024
025import org.springframework.util.Assert;
026import org.springframework.util.ObjectUtils;
027import org.springframework.util.StringUtils;
028
029import static org.hamcrest.MatcherAssert.*;
030import static org.hamcrest.core.IsInstanceOf.*;
031import static org.springframework.test.util.AssertionErrors.*;
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                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 a 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                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, 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                                fail("No matching value at JSON path \"" + this.expression + "\"");
105                        }
106                        if (actualValueList.size() != 1) {
107                                fail("Got a list of values " + actualValue + " instead of the expected single value " + expectedValue);
108                        }
109                        actualValue = actualValueList.get(0);
110                }
111                else if (actualValue != null && expectedValue != null) {
112                        if (!actualValue.getClass().equals(expectedValue.getClass())) {
113                                actualValue = evaluateJsonPath(content, expectedValue.getClass());
114                        }
115                }
116                assertEquals("JSON path \"" + this.expression + "\"", expectedValue, actualValue);
117        }
118
119        /**
120         * Evaluate the JSON path expression against the supplied {@code content}
121         * and assert that the resulting value is a {@link String}.
122         * @param content the JSON content
123         * @since 4.2.1
124         */
125        public void assertValueIsString(String content) {
126                Object value = assertExistsAndReturn(content);
127                assertThat(failureReason("a string", value), value, instanceOf(String.class));
128        }
129
130        /**
131         * Evaluate the JSON path expression against the supplied {@code content}
132         * and assert that the resulting value is a {@link Boolean}.
133         * @param content the JSON content
134         * @since 4.2.1
135         */
136        public void assertValueIsBoolean(String content) {
137                Object value = assertExistsAndReturn(content);
138                assertThat(failureReason("a boolean", value), value, instanceOf(Boolean.class));
139        }
140
141        /**
142         * Evaluate the JSON path expression against the supplied {@code content}
143         * and assert that the resulting value is a {@link Number}.
144         * @param content the JSON content
145         * @since 4.2.1
146         */
147        public void assertValueIsNumber(String content) {
148                Object value = assertExistsAndReturn(content);
149                assertThat(failureReason("a number", value), value, instanceOf(Number.class));
150        }
151
152        /**
153         * Evaluate the JSON path expression against the supplied {@code content}
154         * and assert that the resulting value is an array.
155         * @param content the JSON content
156         */
157        public void assertValueIsArray(String content) {
158                Object value = assertExistsAndReturn(content);
159                assertThat(failureReason("an array", value), value, instanceOf(List.class));
160        }
161
162        /**
163         * Evaluate the JSON path expression against the supplied {@code content}
164         * and assert that the resulting value is a {@link Map}.
165         * @param content the JSON content
166         * @since 4.2.1
167         */
168        public void assertValueIsMap(String content) {
169                Object value = assertExistsAndReturn(content);
170                assertThat(failureReason("a map", value), value, instanceOf(Map.class));
171        }
172
173        /**
174         * Evaluate the JSON path expression against the supplied {@code content}
175         * and assert that a non-null value exists at the given path.
176         * <p>If the JSON path expression is not
177         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
178         * that the value at the given path is not <em>empty</em>.
179         * @param content the JSON content
180         */
181        public void exists(String content) {
182                assertExistsAndReturn(content);
183        }
184
185        /**
186         * Evaluate the JSON path expression against the supplied {@code content}
187         * and assert that a value does not exist at the given path.
188         * <p>If the JSON path expression is not
189         * {@linkplain JsonPath#isDefinite() definite}, this method asserts
190         * that the value at the given path is <em>empty</em>.
191         * @param content the JSON content
192         */
193        public void doesNotExist(String content) {
194                Object value;
195                try {
196                        value = evaluateJsonPath(content);
197                }
198                catch (AssertionError ex) {
199                        return;
200                }
201                String reason = failureReason("no value", value);
202                if (pathIsIndefinite() && value instanceof List) {
203                        assertTrue(reason, ((List<?>) value).isEmpty());
204                }
205                else {
206                        assertTrue(reason, (value == null));
207                }
208        }
209
210        /**
211         * Evaluate the JSON path expression against the supplied {@code content}
212         * and assert that an empty value exists at the given path.
213         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
214         * {@link ObjectUtils#isEmpty(Object)}.
215         * @param content the JSON content
216         */
217        public void assertValueIsEmpty(String content) {
218                Object value = evaluateJsonPath(content);
219                assertTrue(failureReason("an empty value", value), ObjectUtils.isEmpty(value));
220        }
221
222        /**
223         * Evaluate the JSON path expression against the supplied {@code content}
224         * and assert that a non-empty value exists at the given path.
225         * <p>For the semantics of <em>empty</em>, consult the Javadoc for
226         * {@link ObjectUtils#isEmpty(Object)}.
227         * @param content the JSON content
228         */
229        public void assertValueIsNotEmpty(String content) {
230                Object value = evaluateJsonPath(content);
231                assertTrue(failureReason("a non-empty value", value), !ObjectUtils.isEmpty(value));
232        }
233
234        private String failureReason(String expectedDescription, Object value) {
235                return String.format("Expected %s at JSON path \"%s\" but found: %s", expectedDescription, this.expression,
236                                ObjectUtils.nullSafeToString(StringUtils.quoteIfString(value)));
237        }
238
239        private Object evaluateJsonPath(String content) {
240                try {
241                        return this.jsonPath.read(content);
242                }
243                catch (Throwable ex) {
244                        throw new AssertionError("No value at JSON path \"" + this.expression + "\": " + ex);
245                }
246        }
247
248        private Object evaluateJsonPath(String content, Class<?> targetType) {
249                try {
250                        return JsonPath.parse(content).read(this.expression, targetType);
251                }
252                catch (Throwable ex) {
253                        throw new AssertionError("No value at JSON path \"" + this.expression + "\": " + ex);
254                }
255        }
256
257        private Object assertExistsAndReturn(String content) {
258                Object value = evaluateJsonPath(content);
259                String reason = "No value at JSON path \"" + this.expression + "\"";
260                assertTrue(reason, value != null);
261                if (pathIsIndefinite() && value instanceof List) {
262                        assertTrue(reason, !((List<?>) value).isEmpty());
263                }
264                return value;
265        }
266
267        private boolean pathIsIndefinite() {
268                return !this.jsonPath.isDefinite();
269        }
270
271}