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}