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}