001package org.unix4j.builder;
002
003import java.lang.reflect.InvocationHandler;
004import java.lang.reflect.InvocationTargetException;
005import java.lang.reflect.Method;
006import java.lang.reflect.Proxy;
007import java.util.Arrays;
008import java.util.HashMap;
009import java.util.Map;
010
011import org.unix4j.command.Command;
012import org.unix4j.command.CommandInterface;
013
014/**
015 * Generic builder dynamically creating an implementation of an interface
016 * extending {@link CommandBuilder} based on a series of command factories for
017 * all the methods present in that interface.
018 */
019public class GenericCommandBuilder {
020
021        /**
022         * Returns a new command builder implementing the specified
023         * {@code commandBuilderInterface}. The methods in that interface, if not
024         * already implemented by {@link DefaultCommandBuilder}, must be present in
025         * one of the given {@code commandFactories} with the same signature
026         * returning a {@link Command} instance. If no factory is found for any of
027         * the methods in the interface, an exception is thrown.
028         * 
029         * @param <B>
030         *            the generic type of the builder interface to be implemented by
031         *            the returned builder
032         * @param commandBuilderInterface
033         *            the class representing the builder interface to be implemented
034         *            by the returned builder
035         * @param commandFactories
036         *            the factories containing a factory method for every command
037         *            method in the given builder interface
038         * @return a new builder instance implementing the specified interface
039         * @throws IllegalArgumentException
040         *             if {@code commandBuilderInterface} is not an interface class,
041         *             if no factory method implementation exists for any of the
042         *             command methods defined by that interface or if multiple
043         *             ambiguous implementations exist
044         */
045        @SuppressWarnings("unchecked")
046        public static <B extends CommandBuilder> B createCommandBuilder(Class<B> commandBuilderInterface, CommandInterface<? extends Command<?>>... commandFactories) {
047                return createCommandBuilder(commandBuilderInterface, new DefaultCommandBuilder(), commandFactories);
048        }
049
050        /**
051         * Returns a new command builder implementing the specified
052         * {@code commandBuilderInterface}. The methods in that interface, if not
053         * already implemented by the specified {@code defaultCommandBuilder}, must
054         * be present in one of the given {@code commandFactories} with the same
055         * signature returning a {@link Command} instance. If no factory is found
056         * for any of the methods in the interface, an exception is thrown.
057         * 
058         * @param <B>
059         *            the generic type of the builder interface to be implemented by
060         *            the returned builder
061         * @param commandBuilderInterface
062         *            the class representing the builder interface to be implemented
063         *            by the returned builder
064         * @param defaultCommandBuilder
065         *            the default builder with implementations for all non-command
066         *            specific methods
067         * @param commandFactories
068         *            the factories containing a factory method for every command
069         *            method in the given builder interface
070         * @return a new builder instance implementing the specified interface
071         * @throws IllegalArgumentException
072         *             if {@code commandBuilderInterface} is not an interface class,
073         *             if no factory method implementation exists for any of the
074         *             command methods defined by that interface or if multiple
075         *             ambiguous implementations exist
076         */
077        @SuppressWarnings("unchecked")
078        public static <B extends CommandBuilder> B createCommandBuilder(Class<B> commandBuilderInterface, DefaultCommandBuilder defaultCommandBuilder, CommandInterface<? extends Command<?>>... commandFactories) {
079                if (commandBuilderInterface.isInterface()) {
080                        return (B) Proxy.newProxyInstance(GenericCommandBuilder.class.getClassLoader(), new Class[] { commandBuilderInterface }, new GenericCommandHandler<B>(commandBuilderInterface, defaultCommandBuilder, commandFactories));
081                }
082                throw new IllegalArgumentException(commandBuilderInterface.getName() + " must be an interface");
083        }
084
085        private static class GenericCommandHandler<B extends CommandBuilder> implements InvocationHandler {
086                private final DefaultCommandBuilder defaultCommandBuilder;
087                private final Map<MethodSignature, Object> signatureToTarget = new HashMap<GenericCommandBuilder.MethodSignature, Object>();
088
089                @SuppressWarnings("unchecked")
090                public GenericCommandHandler(Class<B> commandBuilderInterface, DefaultCommandBuilder defaultCommandBuilder, CommandInterface<? extends Command<?>>... commandFactories) {
091                        this.defaultCommandBuilder = defaultCommandBuilder;
092                        final Map<MethodSignature, Object> factoryMethods = new HashMap<GenericCommandBuilder.MethodSignature, Object>();
093                        addSignatures(factoryMethods, defaultCommandBuilder);
094                        for (int i = 0; i < commandFactories.length; i++) {
095                                addSignatures(factoryMethods, commandFactories[i]);
096                        }
097                        for (final Method method : defaultCommandBuilder.getClass().getMethods()) {
098                                final MethodSignature signature = new MethodSignature(method);
099                                signatureToTarget.put(signature, defaultCommandBuilder);
100                        }
101                        for (final Method method : commandBuilderInterface.getMethods()) {
102                                final MethodSignature signature = new MethodSignature(method);
103                                final Object factory = factoryMethods.get(signature);
104                                if (factory == null) {
105                                        throw new IllegalArgumentException("No factory method found for method " + signature + " defined in interface " + commandBuilderInterface.getName());
106                                }
107                                final Object otherFactory = signatureToTarget.get(signature);
108                                if (otherFactory == null) {
109                                        signatureToTarget.put(signature, factory);
110                                } else {
111                                        if (factory != otherFactory) {
112                                                throw new IllegalArgumentException("method " + signature + " exist in " + otherFactory.getClass().getName() + " and also in " + factory.getClass().getName());
113                                        }
114                                }
115                        }
116                }
117
118                @Override
119                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
120                        final MethodSignature signature = new MethodSignature(method);
121                        final Object target = signatureToTarget.get(signature);
122                        if (target != null) {
123                                Object result = signature.invoke(target, args);
124                                if (result instanceof Command) {
125                                        result = defaultCommandBuilder.join((Command<?>) result);
126                                }
127                                if (result == defaultCommandBuilder) {
128                                        result = proxy;
129                                }
130                                return result;
131                        }
132                        throw new IllegalStateException("no target object found for method " + signature);
133                }
134
135        }
136
137        private static class MethodSignature {
138                private final String name;
139                private final Class<?>[] parameterTypes;
140
141                public MethodSignature(Method method) {
142                        this(method.getName(), method.getParameterTypes());
143                }
144
145                public MethodSignature(String name, Class<?>... parameterTypes) {
146                        this.name = name;
147                        this.parameterTypes = parameterTypes;
148                }
149
150                public Method findInClass(Class<?> clazz) throws NoSuchMethodException {
151                        return clazz.getMethod(name, parameterTypes);
152                }
153
154                public Object invoke(final Object instance, final Object... args) {
155                        try {
156                                final Method method = findInClass(instance.getClass());
157                                return method.invoke(instance, args);
158                        } catch (InvocationTargetException e) {
159                                if (e.getCause() instanceof RuntimeException) {
160                                        throw (RuntimeException) e.getCause();
161                                }
162                                throw new RuntimeException(e.getCause());
163                        } catch (Exception e) {
164                                throw new RuntimeException("invokation failed for method " + this + " in class " + instance.getClass().getName() + ", e=" + e, e);
165                        }
166                }
167
168                @Override
169                public String toString() {
170                        final StringBuilder sb = new StringBuilder(name).append('(');
171                        for (int i = 0; i < parameterTypes.length; i++) {
172                                if (i > 0)
173                                        sb.append(", ");
174                                sb.append(parameterTypes[i].getName());
175                        }
176                        return sb.append(')').toString();
177                }
178
179                @Override
180                public int hashCode() {
181                        return 31 * name.hashCode() ^ Arrays.hashCode(parameterTypes);
182                }
183
184                @Override
185                public boolean equals(Object obj) {
186                        if (obj == this)
187                                return true;
188                        if (obj instanceof MethodSignature) {
189                                final MethodSignature other = (MethodSignature) obj;
190                                if (!name.equals(other.name))
191                                        return false;
192                                return Arrays.equals(parameterTypes, other.parameterTypes);
193                        }
194                        return false;
195                }
196        }
197
198        private static void addSignatures(Map<MethodSignature, Object> factoryMethods, Object factory) {
199                for (final Method method : factory.getClass().getMethods()) {
200                        final MethodSignature signature = new MethodSignature(method);
201                        final Object existingFactory = factoryMethods.get(signature);
202                        if (existingFactory == null) {
203                                // System.out.println("add method: " + signature + ":\t" +
204                                // factory.getClass());
205                                factoryMethods.put(signature, factory);
206                        } else {
207                                // NOTE: (a) same method exists twice if return type has been
208                                // overloaded
209                                // (b) for object methods and more general all methods defined
210                                // by DefaultCommandBuilder, we use the DefaultCommandBuilder
211                                // method
212                                if (existingFactory != factory && !DefaultCommandBuilder.class.isInstance(existingFactory)) {
213                                        throw new IllegalArgumentException("method " + signature + " exist in " + existingFactory.getClass().getName() + " and also in " + factory.getClass().getName());
214                                }
215                        }
216                }
217        }
218}