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}