/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.json;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Field;
import org.assertj.core.api.Assertions;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
/**
* Base class for AssertJ based JSON marshal testers. Exposes specific Asserts following a
* {@code read}, {@code write} or {@code parse} of JSON content. Typically used in
* combination with an AssertJ {@link Assertions#assertThat(Object) assertThat} call. For
* example: <pre class="code">
* public class ExampleObjectJsonTests {
*
* private AbstractJsonTester<ExampleObject> json = //...
*
* @Test
* public void testWriteJson() {
* ExampleObject object = //...
* assertThat(json.write(object)).isEqualToJson("expected.json");
* assertThat(json.read("expected.json")).isEqualTo(object);
* }
*
* }
* </pre> For a complete list of supported assertions see {@link JsonContentAssert} and
* {@link ObjectContentAssert}.
* <p>
* To use this library JSONAssert must be on the test classpath.
*
* @param <T> the type under test
* @author Phillip Webb
* @since 1.4.0
* @see JsonContentAssert
* @see ObjectContentAssert
*/
public abstract class AbstractJsonMarshalTester<T> {
private Class<?> resourceLoadClass;
private ResolvableType type;
/**
* Create a new uninitialized {@link AbstractJsonMarshalTester} instance.
*/
protected AbstractJsonMarshalTester() {
}
/**
* Create a new {@link AbstractJsonMarshalTester} instance.
* @param resourceLoadClass the source class used when loading relative classpath
* resources
* @param type the type under test
*/
public AbstractJsonMarshalTester(Class<?> resourceLoadClass, ResolvableType type) {
Assert.notNull(resourceLoadClass, "ResourceLoadClass must not be null");
Assert.notNull(type, "Type must not be null");
initialize(resourceLoadClass, type);
}
/**
* Initialize the marshal tester for use.
* @param resourceLoadClass the source class used when loading relative classpath
* resources
* @param type the type under test
*/
protected final void initialize(Class<?> resourceLoadClass, ResolvableType type) {
if (this.resourceLoadClass == null && this.type == null) {
this.resourceLoadClass = resourceLoadClass;
this.type = type;
}
}
/**
* Return the type under test.
* @return the type under test
*/
protected final ResolvableType getType() {
return this.type;
}
/**
* Return class used to load relative resources.
* @return the resource load class
*/
protected final Class<?> getResourceLoadClass() {
return this.resourceLoadClass;
}
/**
* Return {@link JsonContent} from writing the specific value.
* @param value the value to write
* @return the {@link JsonContent}
* @throws IOException on write error
*/
public JsonContent<T> write(T value) throws IOException {
verify();
Assert.notNull(value, "Value must not be null");
String json = writeObject(value, this.type);
return new JsonContent<>(this.resourceLoadClass, this.type, json);
}
/**
* Return the object created from parsing the specific JSON bytes.
* @param jsonBytes the source JSON bytes
* @return the resulting object
* @throws IOException on parse error
*/
public T parseObject(byte[] jsonBytes) throws IOException {
verify();
return parse(jsonBytes).getObject();
}
/**
* Return {@link ObjectContent} from parsing the specific JSON bytes.
* @param jsonBytes the source JSON bytes
* @return the {@link ObjectContent}
* @throws IOException on parse error
*/
public ObjectContent<T> parse(byte[] jsonBytes) throws IOException {
verify();
Assert.notNull(jsonBytes, "JsonBytes must not be null");
return read(new ByteArrayResource(jsonBytes));
}
/**
* Return the object created from parsing the specific JSON String.
* @param jsonString the source JSON string
* @return the resulting object
* @throws IOException on parse error
*/
public T parseObject(String jsonString) throws IOException {
verify();
return parse(jsonString).getObject();
}
/**
* Return {@link ObjectContent} from parsing the specific JSON String.
* @param jsonString the source JSON string
* @return the {@link ObjectContent}
* @throws IOException on parse error
*/
public ObjectContent<T> parse(String jsonString) throws IOException {
verify();
Assert.notNull(jsonString, "JsonString must not be null");
return read(new StringReader(jsonString));
}
/**
* Return the object created from reading from the specified classpath resource.
* @param resourcePath the source resource path. May be a full path or a path relative
* to the {@code resourceLoadClass} passed to the constructor
* @return the resulting object
* @throws IOException on read error
*/
public T readObject(String resourcePath) throws IOException {
verify();
return read(resourcePath).getObject();
}
/**
* Return {@link ObjectContent} from reading from the specified classpath resource.
* @param resourcePath the source resource path. May be a full path or a path relative
* to the {@code resourceLoadClass} passed to the constructor
* @return the {@link ObjectContent}
* @throws IOException on read error
*/
public ObjectContent<T> read(String resourcePath) throws IOException {
verify();
Assert.notNull(resourcePath, "ResourcePath must not be null");
return read(new ClassPathResource(resourcePath, this.resourceLoadClass));
}
/**
* Return the object created from reading from the specified file.
* @param file the source file
* @return the resulting object
* @throws IOException on read error
*/
public T readObject(File file) throws IOException {
verify();
return read(file).getObject();
}
/**
* Return {@link ObjectContent} from reading from the specified file.
* @param file the source file
* @return the {@link ObjectContent}
* @throws IOException on read error
*/
public ObjectContent<T> read(File file) throws IOException {
verify();
Assert.notNull(file, "File must not be null");
return read(new FileSystemResource(file));
}
/**
* Return the object created from reading from the specified input stream.
* @param inputStream the source input stream
* @return the resulting object
* @throws IOException on read error
*/
public T readObject(InputStream inputStream) throws IOException {
verify();
return read(inputStream).getObject();
}
/**
* Return {@link ObjectContent} from reading from the specified input stream.
* @param inputStream the source input stream
* @return the {@link ObjectContent}
* @throws IOException on read error
*/
public ObjectContent<T> read(InputStream inputStream) throws IOException {
verify();
Assert.notNull(inputStream, "InputStream must not be null");
return read(new InputStreamResource(inputStream));
}
/**
* Return the object created from reading from the specified resource.
* @param resource the source resource
* @return the resulting object
* @throws IOException on read error
*/
public T readObject(Resource resource) throws IOException {
verify();
return read(resource).getObject();
}
/**
* Return {@link ObjectContent} from reading from the specified resource.
* @param resource the source resource
* @return the {@link ObjectContent}
* @throws IOException on read error
*/
public ObjectContent<T> read(Resource resource) throws IOException {
verify();
Assert.notNull(resource, "Resource must not be null");
InputStream inputStream = resource.getInputStream();
T object = readObject(inputStream, this.type);
closeQuietly(inputStream);
return new ObjectContent<>(this.type, object);
}
/**
* Return the object created from reading from the specified reader.
* @param reader the source reader
* @return the resulting object
* @throws IOException on read error
*/
public T readObject(Reader reader) throws IOException {
verify();
return read(reader).getObject();
}
/**
* Return {@link ObjectContent} from reading from the specified reader.
* @param reader the source reader
* @return the {@link ObjectContent}
* @throws IOException on read error
*/
public ObjectContent<T> read(Reader reader) throws IOException {
verify();
Assert.notNull(reader, "Reader must not be null");
T object = readObject(reader, this.type);
closeQuietly(reader);
return new ObjectContent<>(this.type, object);
}
private void closeQuietly(Closeable closeable) {
try {
closeable.close();
}
catch (IOException ex) {
}
}
private void verify() {
Assert.state(this.resourceLoadClass != null,
"Uninitialized JsonMarshalTester (ResourceLoadClass is null)");
Assert.state(this.type != null, "Uninitialized JsonMarshalTester (Type is null)");
}
/**
* Write the specified object to a JSON string.
* @param value the source value (never {@code null})
* @param type the resulting type (never {@code null})
* @return the JSON string
* @throws IOException on write error
*/
protected abstract String writeObject(T value, ResolvableType type)
throws IOException;
/**
* Read from the specified input stream to create an object of the specified type. The
* default implementation delegates to {@link #readObject(Reader, ResolvableType)}.
* @param inputStream the source input stream (never {@code null})
* @param type the resulting type (never {@code null})
* @return the resulting object
* @throws IOException on read error
*/
protected T readObject(InputStream inputStream, ResolvableType type)
throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
return readObject(reader, type);
}
/**
* Read from the specified reader to create an object of the specified type.
* @param reader the source reader (never {@code null})
* @param type the resulting type (never {@code null})
* @return the resulting object
* @throws IOException on read error
*/
protected abstract T readObject(Reader reader, ResolvableType type)
throws IOException;
/**
* Utility class used to support field initialization. Used by subclasses to support
* {@code initFields}.
*
* @param <M> The marshaller type
*/
protected static abstract class FieldInitializer<M> {
private final Class<?> testerClass;
@SuppressWarnings("rawtypes")
protected FieldInitializer(
Class<? extends AbstractJsonMarshalTester> testerClass) {
Assert.notNull(testerClass, "TesterClass must not be null");
this.testerClass = testerClass;
}
public void initFields(final Object testInstance, final M marshaller) {
Assert.notNull(testInstance, "TestInstance must not be null");
Assert.notNull(marshaller, "Marshaller must not be null");
initFields(testInstance, new ObjectFactory<M>() {
@Override
public M getObject() throws BeansException {
return marshaller;
}
});
}
public void initFields(final Object testInstance,
final ObjectFactory<M> marshaller) {
Assert.notNull(testInstance, "TestInstance must not be null");
Assert.notNull(marshaller, "Marshaller must not be null");
ReflectionUtils.doWithFields(testInstance.getClass(), new FieldCallback() {
@Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
doWithField(field, testInstance, marshaller);
}
});
}
protected void doWithField(Field field, Object test,
ObjectFactory<M> marshaller) {
if (this.testerClass.isAssignableFrom(field.getType())) {
ReflectionUtils.makeAccessible(field);
Object existingValue = ReflectionUtils.getField(field, test);
if (existingValue == null) {
setupField(field, test, marshaller);
}
}
}
private void setupField(Field field, Object test, ObjectFactory<M> marshaller) {
ResolvableType type = ResolvableType.forField(field).getGeneric();
ReflectionUtils.setField(field, test,
createTester(test.getClass(), type, marshaller.getObject()));
}
protected abstract AbstractJsonMarshalTester<Object> createTester(
Class<?> resourceLoadClass, ResolvableType type, M marshaller);
}
}