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.web.servlet.result; 018 019import java.io.UnsupportedEncodingException; 020 021import com.jayway.jsonpath.JsonPath; 022import org.hamcrest.Matcher; 023import org.hamcrest.MatcherAssert; 024import org.hamcrest.core.StringStartsWith; 025 026import org.springframework.test.util.JsonPathExpectationsHelper; 027import org.springframework.test.web.servlet.MvcResult; 028import org.springframework.test.web.servlet.ResultMatcher; 029import org.springframework.util.StringUtils; 030 031/** 032 * Factory for assertions on the response content using 033 * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expressions. 034 * 035 * <p>An instance of this class is typically accessed via 036 * {@link MockMvcResultMatchers#jsonPath(String, Matcher)} or 037 * {@link MockMvcResultMatchers#jsonPath(String, Object...)}. 038 * 039 * @author Rossen Stoyanchev 040 * @author Craig Andrews 041 * @author Sam Brannen 042 * @author Brian Clozel 043 * @since 3.2 044 */ 045public class JsonPathResultMatchers { 046 047 private final JsonPathExpectationsHelper jsonPathHelper; 048 049 private String prefix; 050 051 052 /** 053 * Protected constructor. 054 * <p>Use {@link MockMvcResultMatchers#jsonPath(String, Object...)} or 055 * {@link MockMvcResultMatchers#jsonPath(String, Matcher)}. 056 * @param expression the {@link JsonPath} expression; never {@code null} or empty 057 * @param args arguments to parameterize the {@code JsonPath} expression with, 058 * using formatting specifiers defined in {@link String#format(String, Object...)} 059 */ 060 protected JsonPathResultMatchers(String expression, Object... args) { 061 this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args); 062 } 063 064 /** 065 * Configures the current {@code JsonPathResultMatchers} instance 066 * to verify that the JSON payload is prepended with the given prefix. 067 * <p>Use this method if the JSON payloads are prefixed to avoid 068 * Cross Site Script Inclusion (XSSI) attacks. 069 * @param prefix the string prefix prepended to the actual JSON payload 070 * @since 4.3 071 */ 072 public JsonPathResultMatchers prefix(String prefix) { 073 this.prefix = prefix; 074 return this; 075 } 076 077 078 /** 079 * Evaluate the JSON path expression against the response content and 080 * assert the resulting value with the given Hamcrest {@link Matcher}. 081 * @see #value(Matcher, Class) 082 * @see #value(Object) 083 */ 084 public <T> ResultMatcher value(final Matcher<T> matcher) { 085 return new ResultMatcher() { 086 @Override 087 public void match(MvcResult result) throws Exception { 088 String content = getContent(result); 089 jsonPathHelper.assertValue(content, matcher); 090 } 091 }; 092 } 093 094 /** 095 * An overloaded variant of {@link #value(Matcher)} that also accepts a 096 * target type for the resulting value that the matcher can work reliably 097 * against. 098 * <p>This can be useful for matching numbers reliably — for example, 099 * to coerce an integer into a double. 100 * @since 4.3.15 101 * @see #value(Matcher) 102 * @see #value(Object) 103 */ 104 public <T> ResultMatcher value(final Matcher<T> matcher, final Class<T> targetType) { 105 return new ResultMatcher() { 106 @Override 107 public void match(MvcResult result) throws Exception { 108 String content = getContent(result); 109 jsonPathHelper.assertValue(content, matcher, targetType); 110 } 111 }; 112 } 113 114 /** 115 * Evaluate the JSON path expression against the response content and 116 * assert that the result is equal to the supplied value. 117 * @see #value(Matcher) 118 * @see #value(Matcher, Class) 119 */ 120 public ResultMatcher value(final Object expectedValue) { 121 return new ResultMatcher() { 122 @Override 123 public void match(MvcResult result) throws Exception { 124 jsonPathHelper.assertValue(getContent(result), expectedValue); 125 } 126 }; 127 } 128 129 /** 130 * Evaluate the JSON path expression against the response content and 131 * assert that a non-null value exists at the given path. 132 * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite 133 * definite}, this method asserts that the value at the given path is not 134 * <em>empty</em>. 135 */ 136 public ResultMatcher exists() { 137 return new ResultMatcher() { 138 @Override 139 public void match(MvcResult result) throws Exception { 140 String content = getContent(result); 141 jsonPathHelper.exists(content); 142 } 143 }; 144 } 145 146 /** 147 * Evaluate the JSON path expression against the response content and 148 * assert that a value does not exist at the given path. 149 * <p>If the JSON path expression is not {@linkplain JsonPath#isDefinite 150 * definite}, this method asserts that the value at the given path is 151 * <em>empty</em>. 152 */ 153 public ResultMatcher doesNotExist() { 154 return new ResultMatcher() { 155 @Override 156 public void match(MvcResult result) throws Exception { 157 String content = getContent(result); 158 jsonPathHelper.doesNotExist(content); 159 } 160 }; 161 } 162 163 /** 164 * Evaluate the JSON path expression against the response content and 165 * assert that an empty value exists at the given path. 166 * <p>For the semantics of <em>empty</em>, consult the Javadoc for 167 * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}. 168 * @since 4.2.1 169 * @see #isNotEmpty() 170 * @see #exists() 171 * @see #doesNotExist() 172 */ 173 public ResultMatcher isEmpty() { 174 return new ResultMatcher() { 175 @Override 176 public void match(MvcResult result) throws Exception { 177 String content = getContent(result); 178 jsonPathHelper.assertValueIsEmpty(content); 179 } 180 }; 181 } 182 183 /** 184 * Evaluate the JSON path expression against the response content and 185 * assert that a non-empty value exists at the given path. 186 * <p>For the semantics of <em>empty</em>, consult the Javadoc for 187 * {@link org.springframework.util.ObjectUtils#isEmpty(Object)}. 188 * @since 4.2.1 189 * @see #isEmpty() 190 * @see #exists() 191 * @see #doesNotExist() 192 */ 193 public ResultMatcher isNotEmpty() { 194 return new ResultMatcher() { 195 @Override 196 public void match(MvcResult result) throws Exception { 197 String content = getContent(result); 198 jsonPathHelper.assertValueIsNotEmpty(content); 199 } 200 }; 201 } 202 203 /** 204 * Evaluate the JSON path expression against the response content and 205 * assert that the result is a {@link String}. 206 * @since 4.2.1 207 */ 208 public ResultMatcher isString() { 209 return new ResultMatcher() { 210 @Override 211 public void match(MvcResult result) throws Exception { 212 String content = getContent(result); 213 jsonPathHelper.assertValueIsString(content); 214 } 215 }; 216 } 217 218 /** 219 * Evaluate the JSON path expression against the response content and 220 * assert that the result is a {@link Boolean}. 221 * @since 4.2.1 222 */ 223 public ResultMatcher isBoolean() { 224 return new ResultMatcher() { 225 @Override 226 public void match(MvcResult result) throws Exception { 227 String content = getContent(result); 228 jsonPathHelper.assertValueIsBoolean(content); 229 } 230 }; 231 } 232 233 /** 234 * Evaluate the JSON path expression against the response content and 235 * assert that the result is a {@link Number}. 236 * @since 4.2.1 237 */ 238 public ResultMatcher isNumber() { 239 return new ResultMatcher() { 240 @Override 241 public void match(MvcResult result) throws Exception { 242 String content = getContent(result); 243 jsonPathHelper.assertValueIsNumber(content); 244 } 245 }; 246 } 247 248 /** 249 * Evaluate the JSON path expression against the response content and 250 * assert that the result is an array. 251 */ 252 public ResultMatcher isArray() { 253 return new ResultMatcher() { 254 @Override 255 public void match(MvcResult result) throws Exception { 256 String content = getContent(result); 257 jsonPathHelper.assertValueIsArray(content); 258 } 259 }; 260 } 261 262 /** 263 * Evaluate the JSON path expression against the response content and 264 * assert that the result is a {@link java.util.Map}. 265 * @since 4.2.1 266 */ 267 public ResultMatcher isMap() { 268 return new ResultMatcher() { 269 @Override 270 public void match(MvcResult result) throws Exception { 271 String content = getContent(result); 272 jsonPathHelper.assertValueIsMap(content); 273 } 274 }; 275 } 276 277 private String getContent(MvcResult result) throws UnsupportedEncodingException { 278 String content = result.getResponse().getContentAsString(); 279 if (StringUtils.hasLength(this.prefix)) { 280 try { 281 String reason = String.format("Expected a JSON payload prefixed with \"%s\" but found: %s", 282 this.prefix, StringUtils.quote(content.substring(0, this.prefix.length()))); 283 MatcherAssert.assertThat(reason, content, StringStartsWith.startsWith(this.prefix)); 284 return content.substring(this.prefix.length()); 285 } 286 catch (StringIndexOutOfBoundsException ex) { 287 throw new AssertionError("JSON prefix \"" + this.prefix + "\" not found: " + ex); 288 } 289 } 290 else { 291 return content; 292 } 293 } 294 295}