package edu.uw.zookeeper.data;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
public enum Serializers {
REGISTRY;
public static Serializers getInstance() {
return REGISTRY;
}
public static interface ByteSerializer<T> {
byte[] toBytes(T input) throws IOException;
}
public static interface ByteDeserializer<T> {
<U extends T> U fromBytes(byte[] bytes, Class<U> type) throws IOException;
}
public static interface ByteCodec<T> extends ByteSerializer<T>, ByteDeserializer<T> {}
public <I,O> O toClass(I input, Class<O> outputType) {
Class<?> inputType = input.getClass();
Serializer method = find(inputType, outputType);
if (method == null) {
throw new IllegalArgumentException(String.format("Unable to serialize %s to %s", input, outputType));
}
O output = method.invoke(outputType, input);
return output;
}
public static enum ToString implements Function<Object, String> {
TO_STRING;
public static ToString getInstance() {
return TO_STRING;
}
@Override
public String apply(Object input) {
Class<?> inputType = input.getClass();
Serializer serializer = Serializers.getInstance().find(inputType, String.class);
if (serializer != null) {
return serializer.invoke(input);
} else {
return String.valueOf(input);
}
}
}
public static final Set<Class<?>> PRIMITIVE_TYPES = ImmutableSet.<Class<?>>of(String.class, byte[].class);
protected final ConcurrentMap<Class<?>, List<Serializer>> registry;
protected final ConcurrentMap<Class<?>, Set<Serializer>> byType;
private Serializers() {
this.registry = Maps.newConcurrentMap();
this.byType = Maps.newConcurrentMap();
}
public List<Serializer> add(Class<?> type) {
List<Serializer> serializers = registry.get(type);
if (serializers == null) {
serializers = Serializer.discover(type);
registry.put(type, serializers);
if (! serializers.isEmpty()) {
for (Serializer e: serializers) {
Class<?>[] types = { e.inputType(), e.outputType() };
for (Class<?> t: types) {
if (PRIMITIVE_TYPES.contains(t)) {
continue;
}
Set<Serializer> byTypeRegistry = byType.get(t);
if (byTypeRegistry == null) {
byType.putIfAbsent(t, Collections.synchronizedSet(Sets.<Serializer>newHashSet()));
byTypeRegistry = byType.get(t);
}
byTypeRegistry.add(e);
}
}
}
}
return serializers;
}
public List<Serializer> get(Class<?> type) {
return registry.get(type);
}
public Serializer find(Class<?> type,
Class<?> inputType, Class<?> outputType) {
List<Serializer> serializers = add(type);
return Serializer.find(serializers, inputType, outputType);
}
public Serializer find(Class<?> inputType, Class<?> outputType) {
Serializer serializer = null;
Class<?>[] types = { inputType, outputType };
for (Class<?> t: types) {
if (PRIMITIVE_TYPES.contains(t)) {
continue;
}
serializer = find(t, inputType, outputType);
if (serializer != null) {
break;
}
}
if (serializer == null) {
for (Class<?> t: types) {
if (PRIMITIVE_TYPES.contains(t)) {
continue;
}
if (byType.containsKey(t)) {
serializer = Serializer.find(byType.get(t), inputType, outputType);
if (serializer != null) {
break;
}
}
}
}
return serializer;
}
public static class Serializer {
public static List<Serializer> discover(Class<?> type) {
List<Serializer> serializers = Lists.newLinkedList();
for (Method method: type.getDeclaredMethods()) {
Serializes annotation = method.getAnnotation(Serializes.class);
if (annotation == null) {
continue;
}
Class<?> input = annotation.from();
if (input.equals(Void.class)) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0) {
input = type;
} else if (parameterTypes.length == 1) {
input = parameterTypes[0];
}
}
if (input.equals(Void.class)) {
throw new IllegalArgumentException(String.format("Unable to determine input type from %s", method));
}
Class<?> output = annotation.to();
if (output.equals(Void.class)) {
output = method.getReturnType();
}
if (output.equals(Void.class)) {
throw new IllegalArgumentException(String.format("Unable to determine output type from %s", method));
}
Serializer serializer = new Serializer(method, input, output);
serializers.add(serializer);
}
return serializers;
}
public static Serializer find(Iterable<Serializer> serializers,
Class<?> inputType, Class<?> outputType) {
Serializer bestMatch = null;
for (Serializer serializer: serializers) {
if (serializer.inputType().isAssignableFrom(inputType)
&& outputType.isAssignableFrom(serializer.outputType())) {
if (bestMatch == null
|| ! serializer.inputType().isAssignableFrom(bestMatch.inputType())
|| ! serializer.outputType().isAssignableFrom(bestMatch.outputType())) {
bestMatch = serializer;
}
}
}
return bestMatch;
}
protected final Method method;
protected final Class<?> inputType;
protected final Class<?> outputType;
public Serializer(
Method method,
Class<?> inputType,
Class<?> outputType) {
this.method = method;
this.inputType = inputType;
this.outputType = outputType;
}
public Method method() {
return method;
}
public Class<?> inputType() {
return inputType;
}
public Class<?> outputType() {
return outputType;
}
@SuppressWarnings("unchecked")
public <O> O invoke(Object obj, Object...inputs) {
Object invObj = obj;
Object[] invInputs = inputs;
if (Modifier.isStatic(method().getModifiers()) && (! (obj instanceof Class<?>))) {
invObj = obj.getClass();
invInputs = new Object[inputs.length + 1];
invInputs[0] = obj;
for (int i=0; i<inputs.length; ++i) {
invInputs[i+1] = inputs[i];
}
}
O output;
try {
output = (O) method().invoke(invObj, invInputs);
} catch (Exception e) {
throw new RuntimeException(String.format("Unable to invoke %s on %s %s", method(), invObj, Arrays.toString(invInputs)), e);
}
return output;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("method", method())
.add("inputType", inputType())
.add("outputType", outputType())
.toString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (! (obj instanceof Serializer)) {
return false;
}
Serializer other = (Serializer)obj;
return Objects.equal(method(), other.method())
&& Objects.equal(inputType(), other.outputType())
&& Objects.equal(outputType(), other.outputType());
}
@Override
public int hashCode() {
return Objects.hashCode(method(), inputType(), outputType());
}
}
}