ReflectionListDispatcher.java

/***************************************************************************
   Copyright 2012 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.util;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.util.types.NotNullP;
import net.metanotion.util.types.Parser;
import net.metanotion.util.types.TypeDictionary;

/** <p>This is a dispatcher implementation that uses reflection to examine the public methods of a class and process
an abstract parameter list. This dispatcher expects the input to be a pair(represented by an instance of
{@link java.util.Map.Entry}), the key is a string representation of the method name/message, and the value is a list
of values to supply as parameters of the method/message. This dispatcher does not deal with overloaded method names,
and assumes that each method name is u`nique.</p>

<p>If you wish to use parameter names rather than an ordered list, you can compose this dispatcher with the
{@link net.metanotion.util.MapToListDispatcher} and use the {@link net.metanotion.util.NamedParameterMapper}(if you
are using the {@link javax.inject.Named} annotation) or the {@link net.metanotion.util.ObjectParameterMapper} (which
only works on Classes compiled with debug info and uses byte code processing, and does NOT work with interfaces!) to
extract the parameter names and map them to the list position associated with the parameter.</p>

<p>In addition, in parameter parsing, this class assumes that all parameters must not be null UNLESS they are
annotated {@link net.metanotion.util.Nullable} to indicate that it is acceptable for a parameter to be null or
missing.</p>

<p>You can also use the {@link net.metanotion.util.types.TypeDictionary}/{@link net.metanotion.util.types.Parser}
combo to make more flexible instances of this class that will accept more varied inputs to parameters by using the
type of the parameter to load a parser that will convert/unmarshal more generic types to the proper type.</p>

	@param <O> The type of the object generated by this instance.
*/
public final class ReflectionListDispatcher<O> implements Dispatcher<O, Map.Entry<String,Iterable>> {
	private static final Logger logger = LoggerFactory.getLogger(ReflectionListDispatcher.class);

	/** This interface defines the two step process to parse and then evaluate a method. */
	private interface Eval {
		/** Evaluate the method against an instance of an object that implements that method.
			@param o The object we're going to invoke the message on.
			@param params The parameters to pass to the method.
			@return The results of evaluating the method on the object.
			@throws RuntimeException if something goes wrong evaluating the method or parsing the return value.
		*/
		public Object eval(Object o, Object[] params) throws RuntimeException;

		/** This is called by the constructor for {@link net.metanotion.util.ReflectionListDispatcher.Msg} to
			convert the parameter list into parsed objects to evaluate.
			@param params The list of parameters to parse.
			@return An array of parsed/converted parameters.
		*/
		public Object[] parse(Iterable params);
	};

	private static final class EvalField implements Eval {
		private final Field f;
		public EvalField(Field f) { this.f =f; }

		@Override public Object eval(final Object o, final Object[] params) {
			try {
				return f.get(o);
			} catch (IllegalAccessException iae) { throw new RuntimeException(iae); }
		}
		@Override public Object[] parse(final Iterable params) { return null; }
	}

	private static final class EvalMethod implements Eval {
		private final Method m;
		private final ArrayList<Parser> parsers = new ArrayList<>();
		public EvalMethod(final Method m, final Dictionary<Class,Parser> types) {
			this.m = m;
			/* Process the parameter annotations and types to construct parameter parsers for each parameter. */
			final Annotation[][] parameterAnnotations = m.getParameterAnnotations();
			final Parser def = types.get(Object.class);
			int i = 0;
			for(final Class c: m.getParameterTypes()) {
				Parser p = types.get(c);
				p = (p==null) ? def : p;
				logger.debug("Parser {}", p);
				boolean nullable = false;
				for(final Annotation a: parameterAnnotations[i]) {
					if(a instanceof Nullable) { nullable = true; }
				}
				parsers.add((nullable) ? p : new NotNullP(p));
				i++;
			}
		}

		@Override public Object[] parse(final Iterable params) {
			logger.debug("Parsing Method params for " + m.getName());
			final Object[] args = new Object[parsers.size()];
			int i=0;
			for(final Object p: params) {
				if(i >= args.length) { break; }
				logger.debug("Param: {} - {} - {}", i, p!=null? p.toString() : "", p!=null? p.getClass() : "");
				try {
					args[i] = parsers.get(i).parse(p);
				} catch (Exception e) { throw new RuntimeException(e); }
				logger.debug(" final {}", args[i]);
				i++;
			}
			return args;
		}

		@Override public Object eval(final Object o, final Object[] args) {
			try {
				logger.debug("Method: " + m.getName());
				return m.invoke(o, args);
			} catch (IllegalAccessException | InvocationTargetException e) {
				throw new RuntimeException(e);
			}
		}
	}

	private static final class Msg<O> implements Message<O> {
		private final Class<O> klass;
		private final Eval method;
		private final Object[] params;
		public Msg(Class<O> klass, Eval method, Iterable params) {
			this.klass = klass;
			this.method = method;
			this.params = method.parse(params);
		}

		@Override public Object call(O o) { return this.method.eval(o, params); }
		@Override public Class<O> receiverType() { return this.klass; }
	}

	/** The type this dispatcher operates on. */
	private final Class<O> klass;

	/** This is a map of the method names to evaluator instances used to parse and invoke the methods. */
	private final Map<String,Eval> accessors = new HashMap<>();

	/** Construct a dispatcher for the class with the default {@link net.metanotion.util.types.TypeDictionary} instance.
		@param klass the class to construct a dispatcher for.
	*/
	public ReflectionListDispatcher(final Class<O> klass) { this(klass, new TypeDictionary()); }

	/** Construct a dispatcher for the class with a custom {@link net.metanotion.util.Dictionary} for type conversion.
		@param klass the class to construct a dispatcher for.
		@param types The type dictionary to use for type conversion.
	*/
	public ReflectionListDispatcher(final Class<O> klass, final Dictionary<Class,Parser> types) {
		this.klass = klass;
		final Field[] fields = klass.getFields();
		for(Field f: fields) {
			f.setAccessible(true);
			accessors.put(f.getName(), new EvalField(f));
		}
		final Method[] methods = klass.getMethods();
		for(Method m: methods) {
			m.setAccessible(true);
			accessors.put(m.getName(), new EvalMethod(m, types));
		}
	}

	@Override public Message<O> dispatch(final Map.Entry<String,Iterable> data) {
		final Eval e = this.accessors.get(data.getKey());
		if(e==null) { throw new RuntimeException("No Such Method " + data.getKey()); }
		return new Msg<>(this.klass, e, data.getValue());
	}
}