package tc.oc.pgm.xml.parser;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.cache.Cache;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import com.google.inject.ImplementedBy;
import com.google.inject.Key;
import com.google.inject.MembersInjector;
import com.google.inject.TypeLiteral;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.jdom2.Element;
import tc.oc.commons.core.formatting.StringUtils;
import tc.oc.commons.core.inject.Injection;
import tc.oc.commons.core.inject.InjectionChecks;
import tc.oc.commons.core.inject.KeyedManifest;
import tc.oc.commons.core.inspect.Inspectable;
import tc.oc.commons.core.inspect.InspectableProperty;
import tc.oc.commons.core.reflect.Members;
import tc.oc.commons.core.reflect.MethodFormException;
import tc.oc.commons.core.reflect.MethodHandleUtils;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.reflect.Types;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.commons.core.util.CacheUtils;
import tc.oc.commons.core.util.ExceptionUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import tc.oc.pgm.xml.NodeSplitter;
import tc.oc.pgm.xml.Parseable;
import tc.oc.pgm.xml.UnrecognizedXMLException;
import tc.oc.pgm.xml.finder.NodeFinder;
import tc.oc.pgm.xml.validate.Validation;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Generates and binds a {@link ReflectiveParser} by reflecting on {@link T}.
*
* Does not bind {@link Parser}.
*/
public class ReflectiveParserManifest<T extends Parseable> extends KeyedManifest {
private final TypeLiteral<T> type;
private final Key<ReflectiveParser<T>> parserKey;
private final Class<?> baseClass;
public ReflectiveParserManifest(TypeLiteral<T> type) {
this(type, Object.class);
}
public ReflectiveParserManifest(TypeLiteral<T> type, Class<?> baseClass) {
this.type = type;
this.parserKey = Key.get(Types.parameterizedTypeLiteral(ReflectiveParser.class, type));
this.baseClass = checkNotNull(baseClass);
checkArgument(type.getRawType().isInterface());
}
@Override
protected Object manifestKey() {
return type;
}
@Override
protected void configure() {
final ImplementedBy implementedBy = type.getRawType().getAnnotation(ImplementedBy.class);
bind(parserKey).toInstance(new ReflectiveParserImpl(
implementedBy != null ? implementedBy.value() : this.baseClass,
Members.annotations(Parseable.Property.class, Methods.declaredMethodsInAncestors(type.getRawType()))
.merge(this::createProperty)
.collect(Collectors.toImmutableList())
));
}
private Property<T, ?> createProperty(Method method, Parseable.Property annotation) {
if(method.getParameterTypes().length > 0) {
throw new MethodFormException(method, "Property method cannot take parameters");
}
final TypeToken<?> outerType = Types.box(TypeToken.of(method.getGenericReturnType()));
// If the property method is callable, and the outer type is not already Optional,
// then it becomes intrinsically Optional. The proxy handles the logic of unwrapping
// the value or calling the method.
final Aggregator aggregator;
if(Methods.isCallable(method) && !Types.isAssignable(Optional.class, outerType)) {
aggregator = new OptionalAggregator<>(outerType);
} else {
aggregator = Aggregator.forType(outerType);
}
return createProperty(method, annotation, aggregator);
}
private <O, I> Property<O, I> createProperty(Method method, Parseable.Property annotation, Aggregator<O, I> aggregator) {
// Generate names/aliases
final String name = StringUtils.nonEmpty(annotation.name())
.orElseGet(() -> method.getName().replace('_', '-'));
final List<String> aliases = ImmutableList.copyOf(annotation.alias());
// Custom NodeFinders
final List<Class<? extends NodeFinder>> finders = Members
.annotation(Parseable.Nodes.class, method)
.map(annot -> Arrays.asList(annot.value()))
.orElse(PropertyParser.DEFAULT_NODE_FINDERS);
// Custom NodeSplitter
final Class<? extends NodeSplitter> splitter = Members
.annotation(Parseable.Split.class, method)
.<Class<? extends NodeSplitter>>map(Parseable.Split::value)
.orElse(NodeSplitter.Atom.class);
// Validations
final Parseable.Validate validate = method.getAnnotation(Parseable.Validate.class);
final ImmutableList.Builder<Class<Validation<? super I>>> validations = ImmutableList.builder();
if(validate != null) {
for(Class<? extends Validation> validation : validate.value()) {
if(!Validation.type(validation).isAssignableFrom(aggregator.innerTypeToken())) {
throw new MethodFormException(method, "Validation " + validation.getSimpleName() +
" is not applicable to property type " + aggregator.innerTypeToken());
}
validations.add((Class<Validation<? super I>>) validation);
}
}
bindProxy(Types.parameterizedTypeLiteral(Parser.class, aggregator.innerTypeLiteral()));
return new Property<>(
method,
new PropertyParser<>(
binder(),
name,
aliases,
finders,
splitter,
aggregator,
validations.build()
)
);
}
private static class Property<O, I> {
final Method method;
final PropertyParser<O, I> parser;
final InspectableProperty inspectableProperty = new InspectableProperty() {
@Override public String name() {
return parser.name();
}
@Override public Object value(Inspectable inspectable) throws Throwable {
return method.invoke(inspectable);
}
};
Property(Method method, PropertyParser<O, I> parser) {
this.method = method;
this.parser = parser;
}
}
private class ReflectiveParserImpl implements ReflectiveParser<T> {
final Class<? extends T> impl;
final List<Property<?, ?>> properties;
final Set<String> propertyNames;
final MembersInjector<T> injector;
ReflectiveParserImpl(Class<?> base, List<Property<?, ?>> properties) {
InjectionChecks.checkInjectableCGLibProxyBase(base);
this.properties = properties;
this.propertyNames = properties.stream()
.flatMap(property -> property.parser.names().stream())
.collect(Collectors.toImmutableSet());
final Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(base);
enhancer.setInterfaces(new Class[]{ type.getRawType() });
enhancer.setCallbackType(MethodInterceptor.class);
enhancer.setUseFactory(true);
this.impl = enhancer.createClass();
this.injector = getMembersInjector((Class<T>) impl);
}
@Override
public T parseElement(Element parent) throws InvalidXMLException {
// Property methods mapped to their parsed values
final ImmutableMap.Builder<Method, Object> builder = ImmutableMap.builder();
// Remember if any property claimed the parent element
final Set<Node> used = new HashSet<>();
for(Property<?, ?> property : properties) {
// Find all nodes for the property
final Set<Node> nodes = property.parser.findNodes(parent)
.collect(Collectors.toImmutableSet());
used.addAll(nodes);
// Parse!
final Object value = property.parser.parse(parent, nodes.stream());
// Handle default value logic
if(Methods.isCallable(property.method)) {
// If the method is callable, then the parsed type is always an Optional (see createProperty() above).
final Optional optional = (Optional) value;
if(optional.isPresent()) {
// If the parsed Optional is present, add it to the map.
// Unwrap it if necessary, to match the method's return type.
builder.put(property.method, Optional.class.isAssignableFrom(property.method.getReturnType()) ? optional : optional.get());
}
// If it's not present, leave it out of the map entirely, so that the proxy will call the method.
} else {
// If the method is not callable, then the value will always match the method return type
builder.put(property.method, value);
}
}
// Throw if any nodes were not parsed by any property
final Node unused = findUnused(Node.of(parent), used);
if(unused != null) {
throw new UnrecognizedXMLException(unused);
};
// Create the proxy and inject it
return ExceptionUtils.propagate(InvalidXMLException.class, Injection.unwrapExceptions(() -> {
final T t = impl.newInstance();
((net.sf.cglib.proxy.Factory) t).setCallback(0, new Dispatcher(parent, builder.build()));
injector.injectMembers(t);
return t;
}));
}
/**
* Search the given node tree for the first completely unused node, if any.
*
* A node is unused if it's not in the used set, and neither are any of its ancestors or descendants.
* Partly used nodes don't count as unused, but they are guaranteed to have a completely unused descendant.
*/
private @Nullable Node findUnused(Node node, Set<Node> used) {
// If node is used, return nothing
if(used.contains(node)) return null;
// Search for unused descendants
final List<Node> children = node.nodes().collect(Collectors.toImmutableList());
Node firstUnused = null;
boolean partlyUsed = false;
for(Node child : children) {
final Node unused = findUnused(child, used);
// Remember the first unused node we find
if(unused != null && firstUnused == null) {
firstUnused = unused;
}
// The child itself is not unused, then some descendant must be used
if(unused != child) {
partlyUsed = true;
}
}
// If all children are completely unused, then this node is unused as well
// Otherwise, return the first completely unused node that we found, if any
return partlyUsed ? firstUnused : node;
}
private class Dispatcher implements MethodInterceptor {
// These are the methods we want to intercept
class Delegate implements Parseable {
@Override
public Optional<Element> sourceElement() {
return sourceNode;
}
@Override
public Map<Method, Object> parsedValues() {
return values;
}
@Override
public String inspectType() {
return type.getRawType().getSimpleName();
}
@Override
public Stream<? extends InspectableProperty> inspectableProperties() {
return Stream.concat(Parseable.super.inspectableProperties(),
properties.stream().map(property -> property.inspectableProperty));
}
}
final Optional<Element> sourceNode;
final ImmutableMap<Method, Object> values;
final Delegate delegate;
private Dispatcher(Element element, Map<Method, Object> values0) {
this.values = ImmutableMap.copyOf(values0);
this.sourceNode = Optional.of(element);
this.delegate = new Delegate();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// Give our delegate a chance to intercept, and cache the decision
if(delegatedMethods.get(method, () -> method.getDeclaringClass() != Object.class &&
Methods.hasOverrideIn(Delegate.class, method))) {
return method.invoke(delegate, args);
}
// If we have a value for the property, return that
final Object value = values.get(method);
if(value != null) return value;
// If there's no value, then the method MUST be callable (or the code is broken).
// This can only fail for an abstract non-property method (which we should probably be checking for).
if(method.isDefault()) {
// invokeSuper doesn't understand default methods
return defaultMethodHandles.get(method)
.bindTo(obj)
.invokeWithArguments(args);
} else {
return proxy.invokeSuper(obj, args);
}
}
}
}
private static final Cache<Method, Boolean> delegatedMethods = CacheUtils.newCache();
private static final LoadingCache<Method, MethodHandle> defaultMethodHandles = CacheUtils.newCache(MethodHandleUtils::defaultMethodHandle);
}