package com.google.sitebricks.conversion;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Primitives;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.sitebricks.conversion.generics.Generics;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import static com.google.sitebricks.conversion.generics.Generics.erase;
import static com.google.sitebricks.conversion.generics.Generics.getExactSuperType;
import static com.google.sitebricks.conversion.generics.Generics.getTypeParameter;
/**
* @author John Patterson (jdpatterson@gmail.com)
*/
@Singleton
public class StandardTypeConverter implements TypeConverter, ConverterRegistry {
Multimap<Type, Converter<?, ?>> convertersBySource = ArrayListMultimap.create();
Multimap<Type, Converter<?, ?>> convertersByTarget = ArrayListMultimap.create();
Multimap<SourceAndTarget, Converter<?, ?>> convertersBySourceAndTarget = ArrayListMultimap.create();
private static final TypeVariable<? extends Class<?>> sourceTypeParameter = Converter.class.getTypeParameters()[0];
private static final TypeVariable<? extends Class<?>> targetTypeParameter = Converter.class.getTypeParameters()[1];
@Inject
public StandardTypeConverter(@SuppressWarnings("rawtypes") Set<Converter> converters) {
for (Converter<?, ?> converter : converters) {
register(converter);
}
}
@Override
public void register(Converter<?, ?> converter) {
// get the source and target types
Type sourceType = sourceType(converter);
Type targetType = targetType(converter);
convertersBySource.put(sourceType, converter);
convertersByTarget.put(targetType, converter);
convertersBySourceAndTarget.put(new SourceAndTarget(sourceType, targetType), converter);
}
public static Type targetType(Converter<?, ?> converter) {
return getTypeParameter(converter.getClass(), targetTypeParameter);
}
public static Type sourceType(Converter<?, ?> converter) {
return getTypeParameter(converter.getClass(), sourceTypeParameter);
}
@Override
@SuppressWarnings("unchecked")
public <T> T convert(final Object source, Type type) {
// special case for handling a null source values
if (source == null) {
return (T) nullValue(type);
}
// check we already have the exact type
if (source.getClass() == type) {
return (T) source;
}
// check if we already have a sub type
if (Generics.isSuperType(type, source.getClass()))
{
return (T) source;
}
// special case for handling empty string
if ("".equals(source) && type != String.class && isEmptyStringNull()) {
return null;
}
Type sourceType = source.getClass();
Class<?> sourceClass = Generics.erase(sourceType);
// conversion of all array types to collections
if (sourceClass.isArray() && Generics.isSuperType(Collection.class, type)) {
return (T) Arrays.asList(source);
}
// conversion of all collections to arrays
Class<?> targetClass = Generics.erase(type);
if (Collection.class.isAssignableFrom(sourceClass) && targetClass.isArray()) {
// TODO: convert collections to arrays
throw new UnsupportedOperationException("Not implemented yet");
}
// use primitive wrapper types
if (type instanceof Class<?> && ((Class<?>) type).isPrimitive()) {
type = Primitives.wrap((Class<?>) type);
}
// look for converters for exact types or super types
Object result = null;
do {
// first try to find a converter in the forward direction
SourceAndTarget key = new SourceAndTarget(sourceType, type);
Collection<Converter<?, ?>> forwards = convertersBySourceAndTarget.get(key);
// stop at the first converter that returns non-null
for (Converter<?, ?> forward : forwards)
if ((result = typeSafeTo(forward, source)) != null) break;
if (result == null) {
// try the reverse direction (target to source)
Collection<Converter<?,?>> reverses = convertersBySourceAndTarget.get(key.reverse());
// stop at the first converter that returns non-null
for (Converter<?, ?> reverse : reverses)
if ((result = typeSafeFrom(reverse, source)) != null) break;
}
// we have no more super classes to try
if (sourceType == Object.class) break;
// try every super type of the source
Class<?> superClass = erase(sourceType).getSuperclass();
sourceType = getExactSuperType(sourceType, superClass);
} while (result == null);
if (result == null)
throw new IllegalStateException("Cannot convert " + source.getClass() + " to " + type);
return (T) result;
}
@Override
public Collection<Converter<?, ?>> converter(Type source, Type target) {
SourceAndTarget key = new SourceAndTarget(source, target);
return convertersBySourceAndTarget.get(key);
}
protected boolean isEmptyStringNull() {
return true;
}
protected Object nullValue(Type type) {
if (type == String.class) {
return "";
}
else return null;
}
@SuppressWarnings("unchecked")
public static <T, S> T typeSafeTo(Converter<?, ?> converter, S source) {
return ((Converter<S, T>) converter).to(source);
}
@SuppressWarnings("unchecked")
public static <T, S> S typeSafeFrom(Converter<?, ?> converter, T source) {
return ((Converter<S, T>) converter).from(source);
}
@Override
public Multimap<Type, Converter<?, ?>> getConvertersBySource() {
return convertersBySource;
}
@Override
public Multimap<Type, Converter<?, ?>> getConvertersByTarget() {
return convertersByTarget;
}
private static final class SourceAndTarget {
private Type source;
private Type target;
public SourceAndTarget(Type source, Type target) {
this.source = source;
this.target = target;
}
public SourceAndTarget reverse() {
return new SourceAndTarget(target, source);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((source == null) ? 0 : source.hashCode());
result = prime * result + ((target == null) ? 0 : target.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SourceAndTarget other = (SourceAndTarget) obj;
if (source == null) {
if (other.source != null)
return false;
} else if (!source.equals(other.source))
return false;
if (target == null) {
if (other.target != null)
return false;
} else if (!target.equals(other.target))
return false;
return true;
}
}
}