001package co.codewizards.cloudstore.core.objectfactory;
002
003import static co.codewizards.cloudstore.core.util.AssertUtil.*;
004
005import java.lang.reflect.Constructor;
006import java.lang.reflect.InvocationTargetException;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.Map;
012import java.util.ServiceLoader;
013
014/**
015 * Factory for objects.
016 * <p>
017 * Instead of invoking {@code new MySomething()}, devs can import {@link ObjectFactoryUtil}<code>.*</code>
018 * statically and then invoke {@link ObjectFactoryUtil#createObject(Class) createObject(MySomething.class)}.
019 * Thus allowing downstream projects to extend the system by providing a replacement-class. For example, if the
020 * replacement-class {@code MyOther} was registered for {@code MySomething}, the method
021 * {@code createObject(MySomething.class)} would return an instance of {@code MyOther} instead of
022 * {@code MySomething}.
023 * <p>
024 * However, it is urgently recommended <i>not</i> to use this approach, whenever it is possible to use a better solution,
025 * preferably a well-defined service (=&gt; {@link ServiceLoader}). There are situations, e.g. data-model-classes
026 * (a.k.a. entities), where services are not possible and the {@code ObjectFactory} is the perfect solution.
027 * <p>
028 * In order to register a sub-class as replacement for a certain base-class, implementors have to provide a
029 * {@link ClassExtension} and register it using the {@link ServiceLoader}-mechanism.
030 * <p>
031 * Important: You should usually <i>not</i> need to access this class directly! Use {@link ObjectFactoryUtil} instead
032 * (statically import its methods).
033 *
034 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
035 */
036public class ObjectFactory {
037
038        private final Map<Class<?>, ClassExtension<?>> baseClass2ClassExtension;
039
040        private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class[0];
041
042        private static final class Holder {
043                public static final ObjectFactory instance = new ObjectFactory();
044        }
045
046        /**
047         * Gets the singleton instance of this {@code ObjectFactory}.
048         * <p>
049         * <b>Important:</b> You should normally <i>not</i> invoke this method directly, but use {@link ObjectFactoryUtil}
050         * instead.
051         * @return the {@code ObjectFactory} instance; never <code>null</code>.
052         */
053        public static ObjectFactory getInstance() {
054                return Holder.instance;
055        }
056
057        protected ObjectFactory() {
058                final Map<Class<?>, ClassExtension<?>> baseClass2ClassExtension = new HashMap<Class<?>, ClassExtension<?>>();
059                for (final ClassExtension<?> classExtension : ServiceLoader.load(ClassExtension.class)) {
060                        final ClassExtension<?> old = baseClass2ClassExtension.get(classExtension.getBaseClass());
061                        if (old == null || old.getPriority() < classExtension.getPriority())
062                                baseClass2ClassExtension.put(classExtension.getBaseClass(), classExtension);
063                        else if (old.getPriority() == classExtension.getPriority())
064                                throw new IllegalStateException("Multiple ClassExtensions registered on the base-class %s with the same priority!");
065                }
066                this.baseClass2ClassExtension = Collections.unmodifiableMap(baseClass2ClassExtension);
067        }
068
069        public <T> Class<? extends T> getExtendingClass(final Class<T> clazz) {
070                Class<? extends T> c = clazz;
071                ClassExtension<? extends T> classExtension;
072                while (null != (classExtension = getClassExtension(c))) {
073                        c = classExtension.getExtendingClass();
074                }
075                return c;
076        }
077
078        @SuppressWarnings("unchecked")
079        public <T> ClassExtension<T> getClassExtension(final Class<T> clazz) {
080                return (ClassExtension<T>) baseClass2ClassExtension.get(clazz);
081        }
082
083        public <T> T createObject(final Class<T> clazz) {
084                return createObject(clazz, (Class<?>[]) null, (Object[]) null);
085        }
086
087        public <T> T createObject(final Class<T> clazz, final Object ... parameters) {
088                return createObject(clazz, (Class<?>[]) null, parameters);
089        }
090
091        public <T> T createObject(final Class<T> clazz, Class<?>[] parameterTypes, final Object ... parameters) {
092                assertNotNull(clazz, "clazz");
093                if (parameterTypes != null && parameters != null) {
094                        if (parameterTypes.length != parameters.length)
095                                throw new IllegalArgumentException(String.format(
096                                                "parameterTypes.length != parameters.length :: %s != %s", parameterTypes.length, parameters.length));
097                }
098
099                if (parameterTypes == null && (parameters == null || parameters.length == 0))
100                        parameterTypes = EMPTY_CLASS_ARRAY;
101
102                final Class<? extends T> c = getExtendingClass(clazz);
103
104                Constructor<? extends T> constructor;
105                if (parameterTypes == null && parameters != null)
106                        constructor = getMatchingConstructor(c, parameters);
107                else {
108                        for (int i = 0; i < parameterTypes.length; ++i) {
109                                if (parameterTypes[i] == null)
110                                        throw new IllegalArgumentException(String.format("parameterTypes[%s] == null", i));
111                        }
112                        try {
113                                constructor = c.getDeclaredConstructor(parameterTypes);
114                        } catch (final NoSuchMethodException e) {
115                                throw new RuntimeException(e);
116                        }
117                }
118
119                constructor.setAccessible(true);
120
121                try {
122                        final T instance = constructor.newInstance(parameters);
123                        return instance;
124                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
125                        throw new RuntimeException(e);
126                }
127        }
128
129        private <T> Constructor<T> getMatchingConstructor(final Class<T> clazz, final Object[] parameters) {
130                assertNotNull(clazz, "clazz");
131                assertNotNull(parameters, "parameters");
132                final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
133                final List<Constructor<T>> constructorsWithSameNumberOfArguments = new LinkedList<Constructor<T>>();
134                for (final Constructor<?> constructor : constructors) {
135                        if (constructor.getParameterTypes().length == parameters.length) {
136                                @SuppressWarnings("unchecked")
137                                final
138                                Constructor<T> con = (Constructor<T>) constructor;
139                                constructorsWithSameNumberOfArguments.add(con);
140                        }
141                }
142
143                if (constructorsWithSameNumberOfArguments.isEmpty())
144                        throw new RuntimeException(new NoSuchMethodException(String.format("The class %s does not have any constructor with %s arguments.", clazz.getName(), parameters.length)));
145
146                if (constructorsWithSameNumberOfArguments.size() == 1)
147                        return constructorsWithSameNumberOfArguments.get(0);
148
149                throw new UnsupportedOperationException(String.format("The class %s has multiple constructors with %s arguments. This is NOT YET SUPPORTED!", clazz.getName(), parameters.length));
150        }
151}