All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.zarbosoft.interface1.Walk Maven / Gradle / Ivy

package com.zarbosoft.interface1;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.zarbosoft.rendaw.common.ChainComparator;
import com.zarbosoft.rendaw.common.Common;
import com.zarbosoft.rendaw.common.Pair;
import org.reflections.Reflections;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.zarbosoft.rendaw.common.Common.iterable;
import static com.zarbosoft.rendaw.common.Common.uncheck;
import static java.util.Arrays.stream;

public class Walk {
	public static class WalkObjectError extends RuntimeException {
		public WalkObjectError(String message, Throwable chain) {
			super(message, chain);
		}
	}

	/**
	 * A helper method to determine the serialized name of an annotated field.
	 *
	 * @param field
	 * @return
	 */
	public static String decideName(final Field field) {
		final Configuration annotation = field.getAnnotation(Configuration.class);
		if (annotation == null || annotation.name().equals(""))
			return field.getName();
		return annotation.name();
	}

	/**
	 * A helper method to determine the serialized name of an annotated class.
	 *
	 * @param klass
	 * @return
	 */
	public static String decideName(final Class klass) {
		final Configuration annotation = klass.getAnnotation(Configuration.class);
		if (annotation == null || annotation.name().equals(""))
			return klass.getName();
		return annotation.name();
	}

	/**
	 * A helper method to determine the serialized name of an annotated enum value.
	 *
	 * @param value
	 * @return
	 */
	public static String decideEnumName(final Enum value) {
		return decideName(uncheck(() -> value.getClass().getField(value.name())));
	}

	public static boolean required(final Field field) {
		final Configuration annotation = field.getAnnotation(Configuration.class);
		return !annotation.optional();
	}

	public static boolean isConcrete(TypeInfo info) {
		return !((Class) info.type).isInterface() && !Modifier.isAbstract(((Class) info.type).getModifiers());
	}

	public static List, Field>> enumValues(final Class enumClass) {
		return stream(enumClass.getEnumConstants()).map(prevalue -> {
			final Enum value = (Enum) prevalue;
			final Field field = uncheck(() -> enumClass.getField(value.name()));
			return new Pair, Field>(value, field);
		}).collect(Collectors.toList());
	}

	private static Stream> getFields(final Class klass) {
		class FieldIterator implements Iterator> {
			private Class at;

			public FieldIterator(final Class klass) {
				at = klass;
			}

			@Override
			public boolean hasNext() {
				return at != null;
			}

			@Override
			public Class next() {
				final Class out = at;
				at = at.getSuperclass();
				return out;
			}
		}
		return Common
				.stream(new FieldIterator(klass))
				.flatMap(klass2 -> stream(klass2.getDeclaredFields()))
				.map(f -> new Pair<>(f.getAnnotation(Configuration.class), new TypeInfo(f)))
				.filter(p -> p.first != null)
				.map(p -> {
					TypeInfo f = p.second;
					try {
						klass.getMethod(f.field.getName(), f.field.getType());
					} catch (final NoSuchMethodException e) {
						if ((f.field.getModifiers() & Modifier.PUBLIC) == 0)
							throw new AssertionError(String.format("Field %s marked for serialization is not public.",
									f
							));
					}
					return p;
				});
	}

	/**
	 * A class that represents a complete type.
	 */
	public static class TypeInfo {

		/**
		 * The base type
		 */
		public final Type type;

		/**
		 * If generic, type parameters (never null).  Otherwise null.
		 */
		public final TypeInfo[] parameters;

		/**
		 * If this refers to the field, the field data (duplicates some info, like type)
		 */
		public final Field field;

		/**
		 * Internal use only.  Although it's okay if you're using it to copy another typeInfo and modify stuff.
		 *
		 * @param field
		 * @param type
		 * @param parameters
		 */
		public TypeInfo(Field field, Type type, TypeInfo[] parameters) {
			this.type = type;
			this.parameters = parameters;
			this.field = field;
		}

		/**
		 * Internal use only.  Although it's okay if you're using it to copy another typeInfo and remove the parameters.
		 *
		 * @param field
		 * @param target
		 */
		public TypeInfo(final Field field, final Type target) {
			if (target instanceof ParameterizedType) {
				this.type = ((ParameterizedType) target).getRawType();
				parameters = stream(((ParameterizedType) target).getActualTypeArguments())
						.map(type1 -> new TypeInfo(type1))
						.toArray(TypeInfo[]::new);
			} else {
				this.type = target;
				this.parameters = null;
			}
			this.field = field;
		}

		/**
		 * Construct a TypeInfo that represents a simple type (no generic arguments, not a field).
		 *
		 * @param target
		 */
		public TypeInfo(final Type target) {
			this(null, target);
		}

		/**
		 * Construct a TypeInfo that represents a generic type.
		 *
		 * @param type
		 * @param parameter
		 */
		public TypeInfo(final Type type, final TypeInfo... parameter) {
			this.type = type;
			this.parameters = parameter;
			this.field = null;
		}

		/**
		 * Construct a TypeInfo that represents a field (including generic types).
		 *
		 * @param f
		 */
		public TypeInfo(final Field f) {
			this.field = f;
			this.type = f.getType();
			if (f.getGenericType() instanceof ParameterizedType)
				this.parameters = stream(((ParameterizedType) f.getGenericType()).getActualTypeArguments())
						.map(type1 -> new TypeInfo(field, type1))
						.toArray(TypeInfo[]::new);
			else
				this.parameters = null;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (!(obj instanceof TypeInfo))
				return false;
			return type == ((TypeInfo) obj).type &&
					field == ((TypeInfo) obj).field &&
					Arrays.equals(parameters, ((TypeInfo) obj).parameters);
		}

		@Override
		public int hashCode() {
			return Objects.hashCode(type, parameters, field);
		}
	}

	private static class Context {
		public final Reflections reflections;
		public final Visitor visitor;
		public Set> seen = new HashSet<>();

		private Context(final Reflections reflections, final Visitor visitor) {
			this.reflections = reflections;
			this.visitor = visitor;
		}
	}

	public interface Visitor {

		T visitString(TypeInfo field);

		T visitLong(TypeInfo field);

		T visitDouble(TypeInfo field);

		T visitBoolean(TypeInfo field);

		T visitEnum(TypeInfo field);

		T visitList(TypeInfo field, T inner);

		T visitSet(TypeInfo field, T inner);

		T visitMap(TypeInfo field, T inner);

		T visitAbstract(TypeInfo field, Class klass, List, T>> derived);

		T visitConcreteShort(Field field, Class klass);

		void visitConcrete(TypeInfo field, List> fields);

		default T visitOther(TypeInfo field) {
			if (field.field == null)
				throw new AssertionError(String.format("Uninterfacable value of type or derived type [%s]",
						field.type
				));
			else
				throw new AssertionError(String.format("Uninterfacable field of type or derived type [%s] in [%s]",
						field.type,
						field.field.getDeclaringClass().getName()
				));
		}

		default TypeInfo visitField(TypeInfo field) {
			return field;
		}
	}

	public interface ObjectVisitor {

		void visitString(String value);

		void visitLong(Long value);

		void visitDouble(Double value);

		void visitBoolean(Boolean value);

		void visitEnum(Enum value);

		void visitListStart(List value);

		void visitListEnd(List value);

		void visitSetStart(Set value);

		void visitSetEnd(Set value);

		void visitMapStart(Map value);

		void visitKeyBegin(String key);

		void visitKeyEnd(String key);

		void visitMapEnd(Map value);

		void visitAbstractBegin(Class klass, Object value);

		void visitAbstractEnd(Class klass, Object value);

		boolean visitConcreteBegin(Class klass, Object value);

		void visitFieldBegin(TypeInfo field, Object value);

		void visitFieldEnd(TypeInfo field, Object value);

		void visitConcreteEnd(Class klass, Object value);

		default void visitOther(final Object value) {
			throw new AssertionError(String.format("Uninterfacable field of type or derived type [%s]",
					value.getClass()
			));
		}

		default Pair transformValue(TypeInfo fieldInfo, Object value) {
			return new Pair<>(fieldInfo, value);
		}
	}

	/**
	 * Walk a type.
	 *
	 * @param reflections
	 * @param root
	 * @param visitor
	 * @param 
	 * @return
	 */
	public static  T walk(
			final Reflections reflections, final TypeInfo root, final Visitor visitor
	) {
		return implementationForType(new Context<>(reflections, visitor), root);
	}

	private static  T implementationForType(
			final Context context, final TypeInfo target
	) {
		if (target.type == String.class) {
			return context.visitor.visitString(target);
		} else if ((target.type == int.class) ||
				(target.type == Integer.class) ||
				(target.type == long.class) ||
				(target.type == Long.class)) {
			return context.visitor.visitLong(target);
		} else if ((target.type == float.class) ||
				(target.type == Float.class) ||
				(target.type == double.class) ||
				(target.type == Double.class)) {
			return context.visitor.visitDouble(target);
		} else if ((target.type == boolean.class) || (target.type == Boolean.class)) {
			return context.visitor.visitBoolean(target);
		} else if (((Class) target.type).isEnum()) {
			return context.visitor.visitEnum(target);
		} else if (List.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized list!");
			return context.visitor.visitList(target, implementationForType(context, target.parameters[0]));
		} else if (java.util.Set.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized set!");
			return context.visitor.visitSet(target, implementationForType(context, target.parameters[0]));
		} else if (Map.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized map!");
			if (target.parameters.length != 2)
				throw new AssertionError("Map does not have exactly 2 parameters!");
			if (target.parameters[0].type != String.class)
				throw new AssertionError("Interfacable maps must have String keys.");
			return context.visitor.visitMap(target, implementationForType(context, target.parameters[1]));
		} else if (((Class) target.type).getAnnotation(Configuration.class) != null) {
			if (!isConcrete(target)) {
				final java.util.Set subclassNames = new HashSet<>();
				final Set> exclude;
				final Set> include;
				if (target.field != null) {
					final Configuration fieldConfig = target.field.getAnnotation(Configuration.class);
					exclude = new HashSet<>(Arrays.asList(fieldConfig.exclude()));
					include = new HashSet<>(Arrays.asList(fieldConfig.include()));
				} else {
					exclude = ImmutableSet.of();
					include = ImmutableSet.of();
				}
				return context.visitor.visitAbstract(
						target,
						(Class) target.type,
						Sets
								.difference(
										context.reflections.getSubTypesOf((Class) target.type),
										ImmutableSet.of(target)
								)
								.stream()
								.map(s -> (Class) s)
								.filter(s -> exclude.isEmpty() || !exclude.contains(s))
								.filter(s -> include.isEmpty() || include.contains(s))
								.filter(s -> !Modifier.isAbstract(s.getModifiers()))
								.filter(s -> s.getAnnotation(Configuration.class) != null)
								.sorted(new ChainComparator().lesserFirst(Type::getTypeName).build())
								.map(s -> {
									String name = decideName(s);
									if (subclassNames.contains(name))
										throw new IllegalArgumentException(String.format(
												"Specific type [%s] of polymorphic type [%s] is ambiguous.",
												name,
												target.type
										));
									subclassNames.add(name);
									return new Pair, T>(
											s,
											(T) implementationForType(context, new TypeInfo(s))
									);
								})
								.collect(Collectors.toList())
				);
			} else {
				try {
					((Class) target.type).getConstructor();
				} catch (final NoSuchMethodException e) {
					throw new AssertionError(String.format(
							"Interface class [%s] has no nullary constructor or constructor is not public (maybe the class isn't static).",
							target.type
					));
				}
				if (!context.seen.contains(target.type)) {
					context.seen.add((Class) target.type);
					context.visitor.visitConcrete(target, getFields((Class) target.type).map(p -> {
						return new Pair<>(p.second,
								implementationForType(context, context.visitor.visitField(p.second))
						);
					}).collect(Collectors.toList()));
				}
				return context.visitor.visitConcreteShort(target.field, (Class) target.type);
			}
		}
		return context.visitor.visitOther(target);
	}

	/**
	 * Walk an object.
	 *
	 * @param target
	 * @param value
	 * @param visitor
	 */
	public static void walk(TypeInfo target, Object value, final ObjectVisitor visitor) {
		if (target.type == String.class) {
			visitor.visitString((String) value);
		} else if ((target.type == int.class) || (target.type == Integer.class)) {
			visitor.visitLong((long) (int) value);
		} else if ((target.type == long.class) || (target.type == Long.class)) {
			visitor.visitLong((Long) value);
		} else if ((target.type == float.class) || (target.type == Float.class)) {
			visitor.visitDouble((double) (float) value);
		} else if ((target.type == double.class) || (target.type == Double.class)) {
			visitor.visitDouble((Double) value);
		} else if ((target.type == boolean.class) || (target.type == Boolean.class)) {
			visitor.visitBoolean((Boolean) value);
		} else if (((Class) target.type).isEnum()) {
			visitor.visitEnum((Enum) value);
		} else if (List.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized list!");
			visitor.visitListStart((List) value);
			for (final Object subvalue : (List) value) {
				walk(target.parameters[0], subvalue, visitor);
			}
			visitor.visitListEnd((List) value);
		} else if (java.util.Set.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized set!");
			visitor.visitSetStart((Set) value);
			for (final Object subvalue : (Set) value) {
				walk(target.parameters[0], subvalue, visitor);
			}
			visitor.visitSetEnd((Set) value);
		} else if (Map.class.isAssignableFrom((Class) target.type)) {
			if (target.parameters == null)
				throw new AssertionError("Unparameterized map!");
			if (target.parameters[0].type != String.class)
				throw new AssertionError("Interfacable maps must have String keys.");
			visitor.visitMapStart((Map) value);
			for (final Map.Entry subvalue : ((Map) value).entrySet()) {
				visitor.visitKeyBegin(subvalue.getKey());
				walk(target.parameters[1], subvalue.getValue(), visitor);
				visitor.visitKeyEnd(subvalue.getKey());
			}
			visitor.visitMapEnd((Map) value);
		} else if (((Class) target.type).getAnnotation(Configuration.class) != null) {
			if (((Class) target.type).isInterface() ||
					Modifier.isAbstract(((Class) target.type).getModifiers())) {
				visitor.visitAbstractBegin((Class) target.type, value);
				walk(new TypeInfo(value.getClass()), value, visitor);
				visitor.visitAbstractEnd((Class) target.type, value);
			} else {
				final boolean enter = visitor.visitConcreteBegin((Class) target.type, value);
				if (enter) {
					for (Pair pair : iterable(getFields((Class) target.type))) {
						Field field = pair.second.field;
						Object subvalue = uncheck(() -> {
							try {
								return ((Class) target.type).getMethod(field.getName()).invoke(value);
							} catch (final NoSuchMethodException e) {
								return field.get(value);
							}
						});
						Configuration configuration = pair.first;
						Pair transformed = visitor.transformValue(pair.second, subvalue);
						TypeInfo subtarget = transformed.first;
						subvalue = transformed.second;
						if (subvalue == null && configuration.optional())
							continue;
						visitor.visitFieldBegin(subtarget, subvalue);
						try {
							walk(subtarget, subvalue, visitor);
						} catch (Exception e) {
							throw new WalkObjectError(String.format("Error walking field [%s] in [%s]",
									field.getName(),
									target.type.getTypeName()
							), e);
						}
						visitor.visitFieldEnd(subtarget, subvalue);
					}
					visitor.visitConcreteEnd((Class) target.type, value);
				}
			}
		} else
			visitor.visitOther(value);
	}

	public static class DefaultVisitor implements Visitor {
		@Override
		public T visitString(final TypeInfo field) {
			return null;
		}

		@Override
		public T visitLong(final TypeInfo field) {
			return null;
		}

		@Override
		public T visitDouble(final TypeInfo field) {
			return null;
		}

		@Override
		public T visitBoolean(final TypeInfo field) {
			return null;
		}

		@Override
		public T visitEnum(TypeInfo field) {
			return null;
		}

		@Override
		public T visitList(final TypeInfo field, final T inner) {
			return null;
		}

		@Override
		public T visitSet(final TypeInfo field, final T inner) {
			return null;
		}

		@Override
		public T visitMap(final TypeInfo field, final T inner) {
			return null;
		}

		@Override
		public T visitAbstract(
				final TypeInfo field, final Class klass, final List, T>> derived
		) {
			return null;
		}

		@Override
		public T visitConcreteShort(final Field field, final Class klass) {
			return null;
		}

		@Override
		public void visitConcrete(
				final TypeInfo field, final List> fields
		) {
		}

		@Override
		public T visitOther(TypeInfo field) {
			return null;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy