Contents
Jackson Type Proxy
This is an interesting piece of content, contributed by Dain S; see JACKSON-137 for additional information.
Proxy Class
Here is JacksonProxy class, which can be used for dynamically creating writer implementations for all kinds of types. It can be especially useful for things like unit tests.
package org.codehaus.jackson;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
public final class JacksonProxy {
private JacksonProxy() {}
public static <T> T newProxyInstance(Class<T> type, Class<?>... additionalTypes) {
ObjectMapper objectMapper = new ObjectMapper();
return newProxyInstance(objectMapper, type, additionalTypes);
}
public static <T> T newProxyInstance(ObjectMapper objectMapper, Class<T> type, Class<?>... additionalTypes) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) classLoader = type.getClassLoader();
Class<?>[] interfaces = new Class<?>[additionalTypes.length + 1];
interfaces[0] = type;
System.arraycopy(additionalTypes, 0, interfaces, 1, additionalTypes.length);
InvocationHandler invocationHandler = new JsonInvocationHandler(objectMapper, interfaces);
Object proxy = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
return type.cast(proxy);
}
public static ObjectMapper getObjectMapper(Object proxy) {
InvocationHandler invocationHandler = Proxy.getInvocationHandler(proxy);
if (invocationHandler instanceof JsonInvocationHandler) {
JsonInvocationHandler jsonInvocationHandler = (JsonInvocationHandler) invocationHandler;
return jsonInvocationHandler.getObjectMapper();
}
throw new IllegalArgumentException("Proxy is not a JacksonProxy");
}
public static class JsonInvocationHandler implements InvocationHandler {
private final ObjectMapper objectMapper;
private final Map<Method, Handler> handlers = new LinkedHashMap<Method, Handler>();
public JsonInvocationHandler(ObjectMapper objectMapper, Class<?>[] interfaces) {
if (objectMapper == null) throw new NullPointerException("mapper is null");
if (interfaces == null) throw new NullPointerException("type is null");
this.objectMapper = objectMapper;
for (Class<?> type : interfaces) {
for (Method method : type.getMethods()) {
String name = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> returnType = method.getReturnType();
if (name.startsWith("read")) {
if (parameterTypes.length == 1 && !returnType.equals(Void.TYPE)) {
ReadHandler handler = new ReadHandler(objectMapper, method.getGenericReturnType(), canThrowIOException(method));
handlers.put(method, handler);
}
} else if (name.startsWith("write")) {
if (parameterTypes.length == 2 && Void.TYPE.equals(returnType)) {
WriteHandler handler = new WriteHandler(objectMapper, null, canThrowIOException(method));
handlers.put(method, handler);
} else if (parameterTypes.length == 1 && !returnType.equals(Void.TYPE)) {
WriteHandler handler = new WriteHandler(objectMapper, returnType, canThrowIOException(method));
handlers.put(method, handler);
}
}
}
}
}
public ObjectMapper getObjectMapper() {
return objectMapper;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Handler handler = handlers.get(method);
if (handler == null) {
throw new UnsupportedOperationException(method.toString());
}
return handler.invoke(args);
}
private static boolean canThrowIOException(Method method) {
for (Class<?> exceptionType : method.getExceptionTypes()) {
if (IOException.class.isAssignableFrom(exceptionType)) {
return true;
}
}
return false;
}
}
private static interface Handler {
Object invoke(Object[] args) throws Throwable;
}
private static class ReadHandler implements Handler {
private final ObjectMapper mapper;
private final JavaType javaType;
private final boolean canThrowIoException;
public ReadHandler(ObjectMapper mapper, Type type, boolean canThrowIoException) {
this.mapper = mapper;
javaType = TypeFactory.fromType(type);
this.canThrowIoException = canThrowIoException;
}
@Override
public Object invoke(Object[] args) throws Throwable {
try {
Object source = args[0];
return read(source);
} catch (IOException e) {
if (canThrowIoException) {
throw e;
} else {
throw new RuntimeJsonMappingException(e);
}
}
}
public Object read(Object source) throws IOException {
if (source == null) {
throw new NullPointerException("source is null");
}
JsonParser parser;
if (source instanceof String) {
String string = (String) source;
parser = mapper.getJsonFactory().createJsonParser(string);
} else if (source instanceof byte[]) {
byte[] bytes = (byte[]) source;
parser = mapper.getJsonFactory().createJsonParser(bytes);
} else if (source instanceof Reader) {
Reader reader = (Reader) source;
parser = mapper.getJsonFactory().createJsonParser(reader);
parser.disableFeature(JsonParser.Feature.AUTO_CLOSE_SOURCE);
} else if (source instanceof InputStream) {
InputStream inputStream = (InputStream) source;
parser = mapper.getJsonFactory().createJsonParser(inputStream);
parser.disableFeature(JsonParser.Feature.AUTO_CLOSE_SOURCE);
} else if (source instanceof File) {
File file = (File) source;
parser = mapper.getJsonFactory().createJsonParser(file);
} else if (source instanceof URL) {
URL url = (URL) source;
parser = mapper.getJsonFactory().createJsonParser(url);
} else {
throw new UnsupportedOperationException("Unsupported source type " + source.getClass() + " for JSON read method");
}
try {
return mapper.readValue(parser, javaType);
} finally {
parser.close();
}
}
}
private static class WriteHandler implements Handler {
private final ObjectMapper mapper;
private final Class<?> returnType;
private final boolean canThrowIoException;
public WriteHandler(ObjectMapper mapper, Class<?> returnType, boolean canThrowIoException) {
this.mapper = mapper;
this.returnType = returnType;
this.canThrowIoException = canThrowIoException;
}
@Override
public Object invoke(Object[] args) throws Throwable {
try {
Object value = args[0];
if (returnType == null) {
Object target = args[1];
write(value, target);
return null;
} else if (String.class.equals(returnType)) {
StringWriter stringWriter = new StringWriter();
write(value, stringWriter);
return stringWriter.toString();
} else if (byte[].class.equals(returnType)) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
write(value, out);
return out.toByteArray();
} else {
throw new UnsupportedOperationException("Unsupported target type " + returnType + " for JSON write method");
}
} catch (IOException e) {
if (canThrowIoException) {
throw e;
} else {
throw new RuntimeJsonMappingException(e);
}
}
}
public void write(Object value, Object target) throws IOException {
if (target == null) {
throw new NullPointerException("target is null");
}
JsonGenerator generator;
if (target instanceof Writer) {
Writer writer = (Writer) target;
generator = mapper.getJsonFactory().createJsonGenerator(writer);
generator.disableFeature(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
} else if (target instanceof OutputStream) {
OutputStream outputStream = (OutputStream) target;
generator = mapper.getJsonFactory().createJsonGenerator(outputStream, JsonEncoding.UTF8);
generator.disableFeature(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
} else if (target instanceof File) {
File file = (File) target;
generator = mapper.getJsonFactory().createJsonGenerator(file, JsonEncoding.UTF8);
} else {
throw new UnsupportedOperationException("Unsupported target type " + target.getClass() + " for JSON write method");
}
if (mapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
generator.useDefaultPrettyPrinter();
}
try {
mapper.writeValue(generator, value);
} finally {
generator.close();
}
}
}
}
Unit tests
To give some idea of how to use proxy, here's the related unit test class:
package org.codehaus.jackson;
import java.io.*;
import java.util.Arrays;
import static java.util.Arrays.asList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class JacksonProxyTest {
private JsonInterface json;
@BeforeMethod
protected void setUp() throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.WRITE_NULL_PROPERTIES, false);
mapper.configure(Feature.INDENT_OUTPUT, true);
json = JacksonProxy.newProxyInstance(mapper, JsonInterface.class, MixinInterface.class);
}
@Test
public void testReadSingle() throws IOException {
Assert.assertEquals(json.readString("\"value\""), "value");
Assert.assertEquals(json.readInt("42"), 42);
Assert.assertEquals(json.readInteger("42"), (Integer) 42);
Assert.assertEquals(json.readBean("{ \"myString\" : \"value\", \"myInt\" : 42 }"), new Bean("value", 42));
}
@Test
public void testReadList() throws IOException {
Assert.assertEquals(json.readStringList("[\"value0\",\"value1\",\"value2\",\"value3\"]"),
asList("value0", "value1", "value2", "value3"));
Assert.assertEquals(json.readIntegerList("[0, 1, 2, 3]"),
asList(0, 1, 2, 3));
Assert.assertTrue(Arrays.equals(json.readIntArray("[0, 1, 2, 3]"),
new int[]{0, 1, 2, 3}));
Assert.assertEquals(json.readBeanList("[{ \"myString\" : \"value0\", \"myInt\" : 0 }, { \"myString\" : \"value1\", \"myInt\" : 1 }]"),
asList(new Bean("value0", 0), new Bean("value1", 1)));
}
@Test
public void testWrite() throws IOException {
Assert.assertEquals(json.readString(json.writeString("value")), "value");
Assert.assertEquals(json.readInt(json.writeInt(42)), 42);
Assert.assertEquals(json.readInteger(json.writeInteger(42)), (Integer) 42);
Assert.assertEquals(json.readBean(json.writeBean(new Bean("value", 42))), new Bean("value", 42));
}
@Test
public void testWriteList() throws IOException {
Assert.assertEquals(json.readStringList(json.writeStringList(asList("value0", "value1", "value2", "value3"))),
asList("value0", "value1", "value2", "value3"));
Assert.assertEquals(json.readIntegerList(json.writeIntegerList(asList(0, 1, 2, 3))),
asList(0, 1, 2, 3));
Assert.assertTrue(Arrays.equals(json.readIntArray(json.writeIntArray(new int[]{0, 1, 2, 3})),
new int[]{0, 1, 2, 3}));
Assert.assertEquals(json.readBeanList(json.writeBeanList(asList(new Bean("value0", 0), new Bean("value1", 1)))),
asList(new Bean("value0", 0), new Bean("value1", 1)));
}
@Test
public void testWriteVariants() {
List<Bean> expected = asList(new Bean("value0", 0), new Bean("value1", 1));
StringWriter writer = new StringWriter();
json.writeBeanList(expected, writer);
Assert.assertEquals(json.readBeanList(writer.toString()), expected);
ByteArrayOutputStream out = new ByteArrayOutputStream();
json.writeBeanList(expected, out);
Assert.assertEquals(json.readBeanList(out.toByteArray()), expected);
}
@Test
public void testMixinInterface() {
MixinInterface mixin = (MixinInterface) json;
Map<String, String> map = mixin.readMap("{ \"key\" : \"value\" }");
Assert.assertEquals(map, Collections.singletonMap("key", "value"));
Map<String, Bean> map2 = mixin.readBeanMap("{ \"key\" : { \"myString\" : \"value0\", \"myInt\" : 0 } }");
Assert.assertEquals(map2, Collections.singletonMap("key", new Bean("value0", 0)));
}
public static interface JsonInterface {
String readString(String value) throws IOException;
int readInt(String value);
Integer readInteger(String value);
Bean readBean(String value);
List<String> readStringList(String value) throws IOException;
int[] readIntArray(String value);
List<Integer> readIntegerList(String value);
List<Bean> readBeanList(String value);
List<Bean> readBeanList(byte[] value);
List<Bean> readBeanList(Reader value);
List<Bean> readBeanList(InputStream value);
String writeString(String value) throws IOException;
String writeInt(int value);
String writeInteger(Integer value);
String writeBean(Bean value);
String writeStringList(List<String> value) throws IOException;
String writeIntArray(int[] value);
String writeIntegerList(List<Integer> value);
String writeBeanList(List<Bean> value);
void writeBeanList(List<Bean> value, Writer writer);
void writeBeanList(List<Bean> value, OutputStream writer);
byte[] writeBeanListToByteArray(List<Bean> value);
}
public static interface MixinInterface {
Map<String, String> readMap(String value);
Map<String, Bean> readBeanMap(String value);
}
public static class Bean {
private String myString;
private int myInt;
public Bean() {
}
public Bean(String myString, int myInt) {
this.myInt = myInt;
this.myString = myString;
}
public int getMyInt() {
return myInt;
}
public void setMyInt(int myInt) {
this.myInt = myInt;
}
public String getMyString() {
return myString;
}
public void setMyString(String myString) {
this.myString = myString;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bean bean = (Bean) o;
return !(myString != null ? !myString.equals(bean.myString) : bean.myString != null) &&
myInt == bean.myInt;
}
@Override
public int hashCode() {
int result = myString != null ? myString.hashCode() : 0;
result = 31 * result + myInt;
return result;
}
}
}