/** * Copyright (C) 2007 Robbie Vanbrabant * * 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 org.garbagecollected.util; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; /** * Creates objects that fake a builder object for a given {@link Builder} type * based interface. *

* Returned builder objects implement {@link Object#toString()} and * forward calls to equals or hashCode * to the {@link Object} class' default {@link Object#equals(Object)} and * {@link Object#hashCode()} implementations. * * @author Robbie Vanbrabant (robbie.vanbrabant@gmail.com) */ public class BuilderFactory { @SuppressWarnings("serial") private static Map, ?> PRIMITIVE_DEFAULTS = new HashMap, Object>() {{ put(int.class, 0); put(long.class, 0L); put(boolean.class, false); put(byte.class, 0); put(short.class, 0); put(float.class, 0.0F); put(double.class, 0.0D); put(char.class, '\u0000'); }}; private final BuilderType type; public BuilderFactory(BuilderType type) { this.type = type; } public BuilderFactory() { this.type = BuilderType.GETTER_SETTER; } @SuppressWarnings("unchecked") // java.lang.reflect.Proxy is not generic public , V> T make(Class spec, BuilderCallback callback) { return (T) Proxy.newProxyInstance(spec.getClassLoader(), new Class[] { spec }, new BuilderInvocationHandler(getSpecification(type, spec), callback)); } /** * Trades compile time error checking for run time error checking for the * sake of code brevity. */ @SuppressWarnings("unchecked") // java.lang.reflect.Proxy is not generic public , V> T makeRisky(final Class spec, final Class target, final Object... constructorArgs) { BuilderCallback callback = new BuilderCallback() { public V call(T builder) throws Exception { Class[] classes = new Class[constructorArgs.length+1]; classes[0] = spec; for (int i = 0; i < constructorArgs.length; i++) { classes[i+1] = constructorArgs[i].getClass(); } Object[] actualArgs = new Object[constructorArgs.length+1]; actualArgs[0] = builder; System.arraycopy(constructorArgs, 0, actualArgs, 1, constructorArgs.length); Constructor constructor = target.getDeclaredConstructor(classes); constructor.setAccessible(true); // Eww.. try { return constructor.newInstance(actualArgs); } finally { constructor.setAccessible(false); } } }; return make(spec, callback); } private BuilderSpecification getSpecification(BuilderType type, Class spec) { switch(type) { case SIMPLE: return new SimpleBuilderSpecification(spec); case SIMPLE_SETTER: return new SimpleSetterBuilderSpecification(spec); case GETTER_SETTER: return new GetterSetterBuilderSpecification(spec); } throw new IllegalArgumentException(); } private static class BuilderInvocationHandler, V> implements InvocationHandler { private final BuilderSpecification spec; private final BuilderCallback callback; private Map methodsToValues = new HashMap(); private BuilderInvocationHandler(BuilderSpecification spec, BuilderCallback callback) { this.callback = callback; this.spec = spec; } @SuppressWarnings("unchecked") // java.lang.reflect.Proxy is not generic public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (isObjectToString(method)) { return toString(); } else if (isObjectHashCode(method)) { return hashCode(); } else if (isObjectEquals(method)) { return equals(args[0]); } else if (spec.isWriter(method, args)) { methodsToValues.put(spec.writerIdentity(method), args[0]); return proxy; } else if (isBuilder(method)) { return callback.call((T) proxy); } else if (spec.isReader(method, args)) { Object value = methodsToValues.get(spec.readerIdentity(method)); if (value == null && method.getReturnType().isPrimitive()) { return PRIMITIVE_DEFAULTS.get(method.getReturnType()); } return value; } else { throw new IllegalStateException(String.format( "method '%s' is not a reader or a writer", method.getName())); } } private boolean isBuilder(Method method) { return method.equals(Builder.class.getMethods()[0]); } private boolean isObjectToString(Method method) { return "toString".equals(method.getName()) && method.getParameterTypes().length == 0; } private boolean isObjectHashCode(Method method) { return "hashCode".equals(method.getName()) && method.getParameterTypes().length == 0; } private boolean isObjectEquals(Method method) { return "equals".equals(method.getName()) && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(Object.class); } public boolean equals(Object that) { if (that == null) return false; if (Proxy.isProxyClass(that.getClass())) return super.equals(Proxy.getInvocationHandler(that)); return false; } public int hashCode() { return super.hashCode(); } public String toString() { return methodsToValues.toString(); } } }