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 — 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}