001/* 002 * Copyright 2012-2017 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 * http://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.boot.test.json; 018 019import java.io.BufferedReader; 020import java.io.Closeable; 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.InputStreamReader; 025import java.io.Reader; 026import java.io.StringReader; 027import java.lang.reflect.Field; 028 029import org.assertj.core.api.Assertions; 030 031import org.springframework.beans.BeansException; 032import org.springframework.beans.factory.ObjectFactory; 033import org.springframework.core.ResolvableType; 034import org.springframework.core.io.ByteArrayResource; 035import org.springframework.core.io.ClassPathResource; 036import org.springframework.core.io.FileSystemResource; 037import org.springframework.core.io.InputStreamResource; 038import org.springframework.core.io.Resource; 039import org.springframework.util.Assert; 040import org.springframework.util.ReflectionUtils; 041import org.springframework.util.ReflectionUtils.FieldCallback; 042 043/** 044 * Base class for AssertJ based JSON marshal testers. Exposes specific Asserts following a 045 * {@code read}, {@code write} or {@code parse} of JSON content. Typically used in 046 * combination with an AssertJ {@link Assertions#assertThat(Object) assertThat} call. For 047 * example: <pre class="code"> 048 * public class ExampleObjectJsonTests { 049 * 050 * private AbstractJsonTester<ExampleObject> json = //... 051 * 052 * @Test 053 * public void testWriteJson() { 054 * ExampleObject object = //... 055 * assertThat(json.write(object)).isEqualToJson("expected.json"); 056 * assertThat(json.read("expected.json")).isEqualTo(object); 057 * } 058 * 059 * } 060 * </pre> For a complete list of supported assertions see {@link JsonContentAssert} and 061 * {@link ObjectContentAssert}. 062 * <p> 063 * To use this library JSONAssert must be on the test classpath. 064 * 065 * @param <T> the type under test 066 * @author Phillip Webb 067 * @since 1.4.0 068 * @see JsonContentAssert 069 * @see ObjectContentAssert 070 */ 071public abstract class AbstractJsonMarshalTester<T> { 072 073 private Class<?> resourceLoadClass; 074 075 private ResolvableType type; 076 077 /** 078 * Create a new uninitialized {@link AbstractJsonMarshalTester} instance. 079 */ 080 protected AbstractJsonMarshalTester() { 081 } 082 083 /** 084 * Create a new {@link AbstractJsonMarshalTester} instance. 085 * @param resourceLoadClass the source class used when loading relative classpath 086 * resources 087 * @param type the type under test 088 */ 089 public AbstractJsonMarshalTester(Class<?> resourceLoadClass, ResolvableType type) { 090 Assert.notNull(resourceLoadClass, "ResourceLoadClass must not be null"); 091 Assert.notNull(type, "Type must not be null"); 092 initialize(resourceLoadClass, type); 093 } 094 095 /** 096 * Initialize the marshal tester for use. 097 * @param resourceLoadClass the source class used when loading relative classpath 098 * resources 099 * @param type the type under test 100 */ 101 protected final void initialize(Class<?> resourceLoadClass, ResolvableType type) { 102 if (this.resourceLoadClass == null && this.type == null) { 103 this.resourceLoadClass = resourceLoadClass; 104 this.type = type; 105 } 106 } 107 108 /** 109 * Return the type under test. 110 * @return the type under test 111 */ 112 protected final ResolvableType getType() { 113 return this.type; 114 } 115 116 /** 117 * Return class used to load relative resources. 118 * @return the resource load class 119 */ 120 protected final Class<?> getResourceLoadClass() { 121 return this.resourceLoadClass; 122 } 123 124 /** 125 * Return {@link JsonContent} from writing the specific value. 126 * @param value the value to write 127 * @return the {@link JsonContent} 128 * @throws IOException on write error 129 */ 130 public JsonContent<T> write(T value) throws IOException { 131 verify(); 132 Assert.notNull(value, "Value must not be null"); 133 String json = writeObject(value, this.type); 134 return new JsonContent<T>(this.resourceLoadClass, this.type, json); 135 } 136 137 /** 138 * Return the object created from parsing the specific JSON bytes. 139 * @param jsonBytes the source JSON bytes 140 * @return the resulting object 141 * @throws IOException on parse error 142 */ 143 public T parseObject(byte[] jsonBytes) throws IOException { 144 verify(); 145 return parse(jsonBytes).getObject(); 146 } 147 148 /** 149 * Return {@link ObjectContent} from parsing the specific JSON bytes. 150 * @param jsonBytes the source JSON bytes 151 * @return the {@link ObjectContent} 152 * @throws IOException on parse error 153 */ 154 public ObjectContent<T> parse(byte[] jsonBytes) throws IOException { 155 verify(); 156 Assert.notNull(jsonBytes, "JsonBytes must not be null"); 157 return read(new ByteArrayResource(jsonBytes)); 158 } 159 160 /** 161 * Return the object created from parsing the specific JSON String. 162 * @param jsonString the source JSON string 163 * @return the resulting object 164 * @throws IOException on parse error 165 */ 166 public T parseObject(String jsonString) throws IOException { 167 verify(); 168 return parse(jsonString).getObject(); 169 } 170 171 /** 172 * Return {@link ObjectContent} from parsing the specific JSON String. 173 * @param jsonString the source JSON string 174 * @return the {@link ObjectContent} 175 * @throws IOException on parse error 176 */ 177 public ObjectContent<T> parse(String jsonString) throws IOException { 178 verify(); 179 Assert.notNull(jsonString, "JsonString must not be null"); 180 return read(new StringReader(jsonString)); 181 } 182 183 /** 184 * Return the object created from reading from the specified classpath resource. 185 * @param resourcePath the source resource path. May be a full path or a path relative 186 * to the {@code resourceLoadClass} passed to the constructor 187 * @return the resulting object 188 * @throws IOException on read error 189 */ 190 public T readObject(String resourcePath) throws IOException { 191 verify(); 192 return read(resourcePath).getObject(); 193 } 194 195 /** 196 * Return {@link ObjectContent} from reading from the specified classpath resource. 197 * @param resourcePath the source resource path. May be a full path or a path relative 198 * to the {@code resourceLoadClass} passed to the constructor 199 * @return the {@link ObjectContent} 200 * @throws IOException on read error 201 */ 202 public ObjectContent<T> read(String resourcePath) throws IOException { 203 verify(); 204 Assert.notNull(resourcePath, "ResourcePath must not be null"); 205 return read(new ClassPathResource(resourcePath, this.resourceLoadClass)); 206 } 207 208 /** 209 * Return the object created from reading from the specified file. 210 * @param file the source file 211 * @return the resulting object 212 * @throws IOException on read error 213 */ 214 public T readObject(File file) throws IOException { 215 verify(); 216 return read(file).getObject(); 217 } 218 219 /** 220 * Return {@link ObjectContent} from reading from the specified file. 221 * @param file the source file 222 * @return the {@link ObjectContent} 223 * @throws IOException on read error 224 */ 225 public ObjectContent<T> read(File file) throws IOException { 226 verify(); 227 Assert.notNull(file, "File must not be null"); 228 return read(new FileSystemResource(file)); 229 } 230 231 /** 232 * Return the object created from reading from the specified input stream. 233 * @param inputStream the source input stream 234 * @return the resulting object 235 * @throws IOException on read error 236 */ 237 public T readObject(InputStream inputStream) throws IOException { 238 verify(); 239 return read(inputStream).getObject(); 240 } 241 242 /** 243 * Return {@link ObjectContent} from reading from the specified input stream. 244 * @param inputStream the source input stream 245 * @return the {@link ObjectContent} 246 * @throws IOException on read error 247 */ 248 public ObjectContent<T> read(InputStream inputStream) throws IOException { 249 verify(); 250 Assert.notNull(inputStream, "InputStream must not be null"); 251 return read(new InputStreamResource(inputStream)); 252 } 253 254 /** 255 * Return the object created from reading from the specified resource. 256 * @param resource the source resource 257 * @return the resulting object 258 * @throws IOException on read error 259 */ 260 public T readObject(Resource resource) throws IOException { 261 verify(); 262 return read(resource).getObject(); 263 } 264 265 /** 266 * Return {@link ObjectContent} from reading from the specified resource. 267 * @param resource the source resource 268 * @return the {@link ObjectContent} 269 * @throws IOException on read error 270 */ 271 public ObjectContent<T> read(Resource resource) throws IOException { 272 verify(); 273 Assert.notNull(resource, "Resource must not be null"); 274 InputStream inputStream = resource.getInputStream(); 275 T object = readObject(inputStream, this.type); 276 closeQuietly(inputStream); 277 return new ObjectContent<T>(this.type, object); 278 } 279 280 /** 281 * Return the object created from reading from the specified reader. 282 * @param reader the source reader 283 * @return the resulting object 284 * @throws IOException on read error 285 */ 286 public T readObject(Reader reader) throws IOException { 287 verify(); 288 return read(reader).getObject(); 289 } 290 291 /** 292 * Return {@link ObjectContent} from reading from the specified reader. 293 * @param reader the source reader 294 * @return the {@link ObjectContent} 295 * @throws IOException on read error 296 */ 297 public ObjectContent<T> read(Reader reader) throws IOException { 298 verify(); 299 Assert.notNull(reader, "Reader must not be null"); 300 T object = readObject(reader, this.type); 301 closeQuietly(reader); 302 return new ObjectContent<T>(this.type, object); 303 } 304 305 private void closeQuietly(Closeable closeable) { 306 try { 307 closeable.close(); 308 } 309 catch (IOException ex) { 310 } 311 } 312 313 private void verify() { 314 Assert.state(this.resourceLoadClass != null, 315 "Uninitialized JsonMarshalTester (ResourceLoadClass is null)"); 316 Assert.state(this.type != null, "Uninitialized JsonMarshalTester (Type is null)"); 317 } 318 319 /** 320 * Write the specified object to a JSON string. 321 * @param value the source value (never {@code null}) 322 * @param type the resulting type (never {@code null}) 323 * @return the JSON string 324 * @throws IOException on write error 325 */ 326 protected abstract String writeObject(T value, ResolvableType type) 327 throws IOException; 328 329 /** 330 * Read from the specified input stream to create an object of the specified type. The 331 * default implementation delegates to {@link #readObject(Reader, ResolvableType)}. 332 * @param inputStream the source input stream (never {@code null}) 333 * @param type the resulting type (never {@code null}) 334 * @return the resulting object 335 * @throws IOException on read error 336 */ 337 protected T readObject(InputStream inputStream, ResolvableType type) 338 throws IOException { 339 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 340 return readObject(reader, type); 341 } 342 343 /** 344 * Read from the specified reader to create an object of the specified type. 345 * @param reader the source reader (never {@code null}) 346 * @param type the resulting type (never {@code null}) 347 * @return the resulting object 348 * @throws IOException on read error 349 */ 350 protected abstract T readObject(Reader reader, ResolvableType type) 351 throws IOException; 352 353 /** 354 * Utility class used to support field initialization. Used by subclasses to support 355 * {@code initFields}. 356 * 357 * @param <M> The marshaller type 358 */ 359 protected static abstract class FieldInitializer<M> { 360 361 private final Class<?> testerClass; 362 363 @SuppressWarnings("rawtypes") 364 protected FieldInitializer( 365 Class<? extends AbstractJsonMarshalTester> testerClass) { 366 Assert.notNull(testerClass, "TesterClass must not be null"); 367 this.testerClass = testerClass; 368 } 369 370 public void initFields(final Object testInstance, final M marshaller) { 371 Assert.notNull(testInstance, "TestInstance must not be null"); 372 Assert.notNull(marshaller, "Marshaller must not be null"); 373 initFields(testInstance, new ObjectFactory<M>() { 374 375 @Override 376 public M getObject() throws BeansException { 377 return marshaller; 378 } 379 380 }); 381 } 382 383 public void initFields(final Object testInstance, 384 final ObjectFactory<M> marshaller) { 385 Assert.notNull(testInstance, "TestInstance must not be null"); 386 Assert.notNull(marshaller, "Marshaller must not be null"); 387 ReflectionUtils.doWithFields(testInstance.getClass(), new FieldCallback() { 388 389 @Override 390 public void doWith(Field field) 391 throws IllegalArgumentException, IllegalAccessException { 392 doWithField(field, testInstance, marshaller); 393 } 394 395 }); 396 } 397 398 protected void doWithField(Field field, Object test, 399 ObjectFactory<M> marshaller) { 400 if (this.testerClass.isAssignableFrom(field.getType())) { 401 ReflectionUtils.makeAccessible(field); 402 Object existingValue = ReflectionUtils.getField(field, test); 403 if (existingValue == null) { 404 setupField(field, test, marshaller); 405 } 406 } 407 } 408 409 private void setupField(Field field, Object test, ObjectFactory<M> marshaller) { 410 ResolvableType type = ResolvableType.forField(field).getGeneric(); 411 ReflectionUtils.setField(field, test, 412 createTester(test.getClass(), type, marshaller.getObject())); 413 } 414 415 protected abstract AbstractJsonMarshalTester<Object> createTester( 416 Class<?> resourceLoadClass, ResolvableType type, M marshaller); 417 418 } 419 420}