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

org.unix4j.builder.GenericCommandBuilder Maven / Gradle / Ivy

There is a newer version: 0.6
Show newest version
package org.unix4j.builder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.unix4j.command.Command;
import org.unix4j.command.CommandInterface;

/**
 * Generic builder dynamically creating an implementation of an interface
 * extending {@link CommandBuilder} based on a series of command factories for
 * all the methods present in that interface.
 */
public class GenericCommandBuilder {

	/**
	 * Returns a new command builder implementing the specified
	 * {@code commandBuilderInterface}. The methods in that interface, if not
	 * already implemented by {@link DefaultCommandBuilder}, must be present in
	 * one of the given {@code commandFactories} with the same signature
	 * returning a {@link Command} instance. If no factory is found for any of
	 * the methods in the interface, an exception is thrown.
	 * 
	 * @param 
	 *            the generic type of the builder interface to be implemented by
	 *            the returned builder
	 * @param commandBuilderInterface
	 *            the class representing the builder interface to be implemented
	 *            by the returned builder
	 * @param commandFactories
	 *            the factories containing a factory method for every command
	 *            method in the given builder interface
	 * @return a new builder instance implementing the specified interface
	 * @throws IllegalArgumentException
	 *             if {@code commandBuilderInterface} is not an interface class,
	 *             if no factory method implementation exists for any of the
	 *             command methods defined by that interface or if multiple
	 *             ambiguous implementations exist
	 */
	public static  B createCommandBuilder(Class commandBuilderInterface, CommandInterface>... commandFactories) {
		return createCommandBuilder(commandBuilderInterface, new DefaultCommandBuilder(), commandFactories);
	}

	/**
	 * Returns a new command builder implementing the specified
	 * {@code commandBuilderInterface}. The methods in that interface, if not
	 * already implemented by the specified {@code defaultCommandBuilder}, must
	 * be present in one of the given {@code commandFactories} with the same
	 * signature returning a {@link Command} instance. If no factory is found
	 * for any of the methods in the interface, an exception is thrown.
	 * 
	 * @param 
	 *            the generic type of the builder interface to be implemented by
	 *            the returned builder
	 * @param commandBuilderInterface
	 *            the class representing the builder interface to be implemented
	 *            by the returned builder
	 * @param defaultCommandBuilder
	 *            the default builder with implementations for all non-command
	 *            specific methods
	 * @param commandFactories
	 *            the factories containing a factory method for every command
	 *            method in the given builder interface
	 * @return a new builder instance implementing the specified interface
	 * @throws IllegalArgumentException
	 *             if {@code commandBuilderInterface} is not an interface class,
	 *             if no factory method implementation exists for any of the
	 *             command methods defined by that interface or if multiple
	 *             ambiguous implementations exist
	 */
	@SuppressWarnings("unchecked")
	public static  B createCommandBuilder(Class commandBuilderInterface, DefaultCommandBuilder defaultCommandBuilder, CommandInterface>... commandFactories) {
		if (commandBuilderInterface.isInterface()) {
			return (B) Proxy.newProxyInstance(GenericCommandBuilder.class.getClassLoader(), new Class[] { commandBuilderInterface }, new GenericCommandHandler(commandBuilderInterface, defaultCommandBuilder, commandFactories));
		}
		throw new IllegalArgumentException(commandBuilderInterface.getName() + " must be an interface");
	}

	private static class GenericCommandHandler implements InvocationHandler {
		private final DefaultCommandBuilder defaultCommandBuilder;
		private final Map signatureToTarget = new HashMap();

		public GenericCommandHandler(Class commandBuilderInterface, DefaultCommandBuilder defaultCommandBuilder, CommandInterface>... commandFactories) {
			this.defaultCommandBuilder = defaultCommandBuilder;
			final Map factoryMethods = new HashMap();
			addSignatures(factoryMethods, defaultCommandBuilder);
			for (int i = 0; i < commandFactories.length; i++) {
				addSignatures(factoryMethods, commandFactories[i]);
			}
			for (final Method method : defaultCommandBuilder.getClass().getMethods()) {
				final MethodSignature signature = new MethodSignature(method);
				signatureToTarget.put(signature, defaultCommandBuilder);
			}
			for (final Method method : commandBuilderInterface.getMethods()) {
				final MethodSignature signature = new MethodSignature(method);
				final Object factory = factoryMethods.get(signature);
				if (factory == null) {
					throw new IllegalArgumentException("No factory method found for method " + signature + " defined in interface " + commandBuilderInterface.getName());
				}
				final Object otherFactory = signatureToTarget.get(signature);
				if (otherFactory == null) {
					signatureToTarget.put(signature, factory);
				} else {
					if (factory != otherFactory) {
						throw new IllegalArgumentException("method " + signature + " exist in " + otherFactory.getClass().getName() + " and also in " + factory.getClass().getName());
					}
				}
			}
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			final MethodSignature signature = new MethodSignature(method);
			final Object target = signatureToTarget.get(signature);
			if (target != null) {
				Object result = signature.invoke(target, args);
				if (result instanceof Command) {
					result = defaultCommandBuilder.join((Command) result);
				}
				if (result == defaultCommandBuilder) {
					result = proxy;
				}
				return result;
			}
			throw new IllegalStateException("no target object found for method " + signature);
		}

	}

	private static class MethodSignature {
		private final String name;
		private final Class[] parameterTypes;

		public MethodSignature(Method method) {
			this(method.getName(), method.getParameterTypes());
		}

		public MethodSignature(String name, Class... parameterTypes) {
			this.name = name;
			this.parameterTypes = parameterTypes;
		}

		public Method findInClass(Class clazz) throws NoSuchMethodException {
			return clazz.getMethod(name, parameterTypes);
		}

		public Object invoke(final Object instance, final Object... args) {
			try {
				final Method method = findInClass(instance.getClass());
				return method.invoke(instance, args);
			} catch (InvocationTargetException e) {
				if (e.getCause() instanceof RuntimeException) {
					throw (RuntimeException) e.getCause();
				}
				throw new RuntimeException(e.getCause());
			} catch (Exception e) {
				throw new RuntimeException("invokation failed for method " + this + " in class " + instance.getClass().getName() + ", e=" + e, e);
			}
		}

		@Override
		public String toString() {
			final StringBuilder sb = new StringBuilder(name).append('(');
			for (int i = 0; i < parameterTypes.length; i++) {
				if (i > 0)
					sb.append(", ");
				sb.append(parameterTypes[i].getName());
			}
			return sb.append(')').toString();
		}

		@Override
		public int hashCode() {
			return 31 * name.hashCode() ^ Arrays.hashCode(parameterTypes);
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == this)
				return true;
			if (obj instanceof MethodSignature) {
				final MethodSignature other = (MethodSignature) obj;
				if (!name.equals(other.name))
					return false;
				return Arrays.equals(parameterTypes, other.parameterTypes);
			}
			return false;
		}
	}

	private static void addSignatures(Map factoryMethods, Object factory) {
		for (final Method method : factory.getClass().getMethods()) {
			final MethodSignature signature = new MethodSignature(method);
			final Object existingFactory = factoryMethods.get(signature);
			if (existingFactory == null) {
				// System.out.println("add method: " + signature + ":\t" +
				// factory.getClass());
				factoryMethods.put(signature, factory);
			} else {
				// NOTE: (a) same method exists twice if return type has been
				// overloaded
				// (b) for object methods and more general all methods defined
				// by DefaultCommandBuilder, we use the DefaultCommandBuilder
				// method
				if (existingFactory != factory && !DefaultCommandBuilder.class.isInstance(existingFactory)) {
					throw new IllegalArgumentException("method " + signature + " exist in " + existingFactory.getClass().getName() + " and also in " + factory.getClass().getName());
				}
			}
		}
	}
}