package co.codewizards.cloudstore.core.objectfactory; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ServiceLoader; /** * Factory for objects. * <p> * Instead of invoking {@code new MySomething()}, devs can import {@link ObjectFactoryUtil}<code>.*</code> * statically and then invoke {@link ObjectFactoryUtil#createObject(Class) createObject(MySomething.class)}. * Thus allowing downstream projects to extend the system by providing a replacement-class. For example, if the * replacement-class {@code MyOther} was registered for {@code MySomething}, the method * {@code createObject(MySomething.class)} would return an instance of {@code MyOther} instead of * {@code MySomething}. * <p> * However, it is urgently recommended <i>not</i> to use this approach, whenever it is possible to use a better solution, * preferably a well-defined service (=> {@link ServiceLoader}). There are situations, e.g. data-model-classes * (a.k.a. entities), where services are not possible and the {@code ObjectFactory} is the perfect solution. * <p> * In order to register a sub-class as replacement for a certain base-class, implementors have to provide a * {@link ClassExtension} and register it using the {@link ServiceLoader}-mechanism. * <p> * Important: You should usually <i>not</i> need to access this class directly! Use {@link ObjectFactoryUtil} instead * (statically import its methods). * * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co */ public class ObjectFactory { private final Map<Class<?>, ClassExtension<?>> baseClass2ClassExtension; private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0]; private static final class Holder { public static final ObjectFactory instance = new ObjectFactory(); } /** * Gets the singleton instance of this {@code ObjectFactory}. * <p> * <b>Important:</b> You should normally <i>not</i> invoke this method directly, but use {@link ObjectFactoryUtil} * instead. * @return the {@code ObjectFactory} instance; never <code>null</code>. */ public static ObjectFactory getInstance() { return Holder.instance; } protected ObjectFactory() { final Map<Class<?>, ClassExtension<?>> baseClass2ClassExtension = new HashMap<Class<?>, ClassExtension<?>>(); for (final ClassExtension<?> classExtension : ServiceLoader.load(ClassExtension.class)) { final ClassExtension<?> old = baseClass2ClassExtension.get(classExtension.getBaseClass()); if (old == null || old.getPriority() < classExtension.getPriority()) baseClass2ClassExtension.put(classExtension.getBaseClass(), classExtension); else if (old.getPriority() == classExtension.getPriority()) throw new IllegalStateException("Multiple ClassExtensions registered on the base-class %s with the same priority!"); } this.baseClass2ClassExtension = Collections.unmodifiableMap(baseClass2ClassExtension); } public <T> Class<? extends T> getExtendingClass(final Class<T> clazz) { Class<? extends T> c = clazz; ClassExtension<? extends T> classExtension; while (null != (classExtension = getClassExtension(c))) { c = classExtension.getExtendingClass(); } return c; } @SuppressWarnings("unchecked") public <T> ClassExtension<T> getClassExtension(final Class<T> clazz) { return (ClassExtension<T>) baseClass2ClassExtension.get(clazz); } public <T> T createObject(final Class<T> clazz) { return createObject(clazz, (Class<?>[]) null, (Object[]) null); } public <T> T createObject(final Class<T> clazz, final Object ... parameters) { return createObject(clazz, (Class<?>[]) null, parameters); } public <T> T createObject(final Class<T> clazz, Class<?>[] parameterTypes, final Object ... parameters) { assertNotNull(clazz, "clazz"); if (parameterTypes != null && parameters != null) { if (parameterTypes.length != parameters.length) throw new IllegalArgumentException(String.format( "parameterTypes.length != parameters.length :: %s != %s", parameterTypes.length, parameters.length)); } if (parameterTypes == null && (parameters == null || parameters.length == 0)) parameterTypes = EMPTY_CLASS_ARRAY; final Class<? extends T> c = getExtendingClass(clazz); Constructor<? extends T> constructor; if (parameterTypes == null && parameters != null) constructor = getMatchingConstructor(c, parameters); else { for (int i = 0; i < parameterTypes.length; ++i) { if (parameterTypes[i] == null) throw new IllegalArgumentException(String.format("parameterTypes[%s] == null", i)); } try { constructor = c.getDeclaredConstructor(parameterTypes); } catch (final NoSuchMethodException e) { throw new RuntimeException(e); } } constructor.setAccessible(true); try { final T instance = constructor.newInstance(parameters); return instance; } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } private <T> Constructor<T> getMatchingConstructor(final Class<T> clazz, final Object[] parameters) { assertNotNull(clazz, "clazz"); assertNotNull(parameters, "parameters"); final Constructor<?>[] constructors = clazz.getDeclaredConstructors(); final List<Constructor<T>> constructorsWithSameNumberOfArguments = new LinkedList<Constructor<T>>(); for (final Constructor<?> constructor : constructors) { if (constructor.getParameterTypes().length == parameters.length) { @SuppressWarnings("unchecked") final Constructor<T> con = (Constructor<T>) constructor; constructorsWithSameNumberOfArguments.add(con); } } if (constructorsWithSameNumberOfArguments.isEmpty()) throw new RuntimeException(new NoSuchMethodException(String.format("The class %s does not have any constructor with %s arguments.", clazz.getName(), parameters.length))); if (constructorsWithSameNumberOfArguments.size() == 1) return constructorsWithSameNumberOfArguments.get(0); throw new UnsupportedOperationException(String.format("The class %s has multiple constructors with %s arguments. This is NOT YET SUPPORTED!", clazz.getName(), parameters.length)); } }