package openperipheral.converter; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import javax.annotation.Nullable; import openmods.reflection.TypeUtils; import openmods.utils.CachedFactory; import openperipheral.api.converter.IConverter; import openperipheral.api.struct.ScriptStruct; import openperipheral.api.struct.ScriptStruct.Output; import openperipheral.api.struct.StructField; public class StructHandlerProvider { public static class InvalidStructureException extends RuntimeException { private static final long serialVersionUID = 1L; public InvalidStructureException(Class<?> cls, Throwable cause) { super("Invalid structure: " + cls, cause); } public InvalidStructureException(String message) { super(message); } } public static final StructHandlerProvider instance = new StructHandlerProvider(); public interface IFieldHandler { public int index(); public String name(); public Type type(); public boolean isOptional(); public Object get(Object target); public void set(Object target, Object value); } public interface IStructHandler { public Object toJava(IConverter converter, Map<?, ?> obj, int indexOffset); public Map<?, ?> fromJava(IConverter converter, Object obj, int indexOffset); public IFieldHandler field(String name); public List<IFieldHandler> fields(); public ScriptStruct.Output defaultOutput(); } private static final Ordering<Field> FIELD_NAME_ORDERING = Ordering.natural().onResultOf(new Function<Field, String>() { @Override public String apply(@Nullable Field input) { return input != null? input.getName() : ""; } }); private static class FieldHandler implements IFieldHandler { private final Type type; private final Field field; private final String name; private final int index; private final boolean isOptional; public FieldHandler(Class<?> ownerCls, Field field, String name, int index, boolean isOptional) { TypeToken<?> fieldType = TypeUtils.resolveFieldType(ownerCls, field); this.type = fieldType.getType(); this.field = field; this.name = name; this.index = index; this.isOptional = isOptional; } @Override public Type type() { return type; } @Override public boolean isOptional() { return isOptional; } @Override public int index() { return index; } @Override public String name() { return name; } @Override public Object get(Object target) { try { return field.get(target); } catch (Exception ex) { throw new RuntimeException("Failed to get value of field " + field, ex); } } @Override public void set(Object target, Object value) { try { field.set(target, value); } catch (Exception ex) { throw new RuntimeException("Failed to set value of field " + field, ex); } } } private static class StructHandler implements IStructHandler { private final Constructor<?> ctor; private final Map<String, IFieldHandler> namedFields; private final List<IFieldHandler> indexedFields; private final Set<IFieldHandler> optionalFields; private final ScriptStruct.Output output; public StructHandler(ScriptStruct meta, Constructor<?> ctor) { this.ctor = ctor; this.output = meta.defaultOutput(); ImmutableSet.Builder<IFieldHandler> optionalFields = ImmutableSet.builder(); final Class<?> cls = this.ctor.getDeclaringClass(); final List<Field> sortedFields = Lists.newArrayList(cls.getFields()); Collections.sort(sortedFields, FIELD_NAME_ORDERING); final SortedMap<Integer, IFieldHandler> indexedFields = Maps.newTreeMap(); int autoIndex = 0; for (Field field : sortedFields) { final StructField fieldMarker = field.getAnnotation(StructField.class); if (fieldMarker == null) continue; final boolean isOptional = fieldMarker.optional(); final int markerIndex = fieldMarker.index(); final int index = (markerIndex != StructField.AUTOASSIGN)? markerIndex : autoIndex; autoIndex++; FieldHandler handler = new FieldHandler(cls, field, field.getName(), index, isOptional); final IFieldHandler prev = indexedFields.put(index, handler); if (prev != null) throw new IllegalArgumentException(String.format("Duplicate index %d on fields %s and %s", index, handler.name(), prev.name())); if (isOptional) optionalFields.add(handler); } this.optionalFields = optionalFields.build(); final int fieldCount = indexedFields.size(); IFieldHandler[] collectedFields = new IFieldHandler[fieldCount]; for (IFieldHandler handler : indexedFields.values()) { final int index = handler.index(); Preconditions.checkArgument(index >= 0, "Negative index on field %s", handler.name()); Preconditions.checkArgument(index < fieldCount, "Non-continuous field numbering on field %s (max index allowed: %s)", handler.name(), fieldCount - 1); collectedFields[index] = handler; } this.indexedFields = ImmutableList.copyOf(collectedFields); ImmutableMap.Builder<String, IFieldHandler> namedFields = ImmutableMap.builder(); for (IFieldHandler handler : collectedFields) { Preconditions.checkArgument(handler != null, "Non-continuous field numbering"); namedFields.put(handler.name(), handler); } this.namedFields = namedFields.build(); } @Override public Object toJava(IConverter converter, Map<?, ?> obj, int indexOffset) { final Object result; try { result = ctor.newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to create object", e); } Set<IFieldHandler> safeFields = Sets.newIdentityHashSet(); safeFields.addAll(optionalFields); for (Map.Entry<?, ?> e : obj.entrySet()) { Object key = e.getKey(); Object value = e.getValue(); if (key instanceof String) { final IFieldHandler f = namedFields.get(key); Preconditions.checkArgument(f != null, "Extraneous field: %s = %s", key, value); setField(converter, result, key, f, value); safeFields.add(f); } else if (key instanceof Number) { final int index = ((Number)key).intValue() - indexOffset; Preconditions.checkArgument(index < indexedFields.size(), "Index %s is outside of allowed range for structure", index); final IFieldHandler f = indexedFields.get(index); Preconditions.checkArgument(f != null, "Extraneous field: %s = %s", key, value); setField(converter, result, key, f, value); safeFields.add(f); } else { throw new IllegalArgumentException(String.format("Extraneous field %s = %s", key, value)); } } for (Map.Entry<String, IFieldHandler> e : namedFields.entrySet()) if (!safeFields.contains(e.getValue())) throw new IllegalArgumentException(String.format("Field %s not set", e.getKey())); return result; } private static void setField(IConverter converter, Object obj, Object fieldKey, IFieldHandler field, Object value) { final Object converted = convertToJava(converter, field, fieldKey, value); field.set(obj, converted); } private static Object convertToJava(IConverter converter, IFieldHandler field, Object key, Object value) { try { return converter.toJava(value, field.type()); } catch (Exception ex) { throw new RuntimeException("Failed to convert field " + key, ex); } } private static Object convertFromJava(IConverter converter, IFieldHandler field, Object key, Object value) { try { return converter.fromJava(value); } catch (Exception ex) { throw new RuntimeException("Failed to convert field " + key, ex); } } @Override public Map<?, ?> fromJava(IConverter converter, Object obj, final int indexOffset) { if (output == Output.OBJECT) { final Map<String, Object> result = Maps.newHashMap(); for (Map.Entry<String, IFieldHandler> e : namedFields.entrySet()) addFieldFromJava(converter, obj, result, e.getKey(), e.getValue()); return result; } else { final Map<Integer, Object> result = Maps.newHashMap(); int index = indexOffset; for (IFieldHandler handler : indexedFields) addFieldFromJava(converter, obj, result, index++, handler); return result; } } private static <T> void addFieldFromJava(IConverter converter, Object obj, Map<T, Object> result, T key, IFieldHandler f) { final Object value = f.get(obj); final Object converted = convertFromJava(converter, f, key, value); result.put(key, converted); } @Override public List<IFieldHandler> fields() { return indexedFields; } @Override public IFieldHandler field(String name) { return namedFields.get(name); } @Override public ScriptStruct.Output defaultOutput() { return output; } } private final CachedFactory<Class<?>, Boolean> checkedClasses = new CachedFactory<Class<?>, Boolean>() { @Override protected Boolean create(Class<?> key) { return key.getAnnotation(ScriptStruct.class) != null; } }; private final CachedFactory<Class<?>, IStructHandler> handlers = new CachedFactory<Class<?>, IStructHandler>() { @Override protected IStructHandler create(Class<?> cls) { if (cls.getEnclosingClass() != null && !Modifier.isStatic(cls.getModifiers())) throw new InvalidStructureException("Can't create serializer for not-static inner " + cls); final ScriptStruct struct = cls.getAnnotation(ScriptStruct.class); if (struct == null) throw new InvalidStructureException("Trying to generate serializer for unserializable " + cls); try { final Constructor<?> ctor = cls.getConstructor(); return new StructHandler(struct, ctor); } catch (Exception e) { throw new InvalidStructureException(cls, e); } } }; public boolean isStruct(Class<?> cls) { return checkedClasses.getOrCreate(cls); } public IStructHandler getHandler(Class<?> cls) { return handlers.getOrCreate(cls); } }