001    /*
002     * Copyright (c) 1998-2014 ChemAxon Ltd. All Rights Reserved.
003     *
004     * This software is the confidential and proprietary information of
005     * ChemAxon. You shall not disclose such Confidential Information
006     * and shall use it only in accordance with the terms of the agreements
007     * you entered into with ChemAxon.
008     *
009     */
010    
011    package com.chemaxon.apidiscovery;
012    
013    import com.chemaxon.apidiscovery.annotations.BuilderClass;
014    import com.chemaxon.apidiscovery.annotations.Creator;
015    import com.chemaxon.apidiscovery.annotations.Description;
016    import com.chemaxon.apidiscovery.annotations.Parameter;
017    import com.chemaxon.apidiscovery.annotations.Parametrized;
018    import com.chemaxon.apidiscovery.interfaces.ParameterBuilder;
019    import com.chemaxon.common.annotations.PublicAPI;
020    import com.google.common.annotations.Beta;
021    import com.google.common.base.Optional;
022    import com.google.common.base.Preconditions;
023    import com.google.common.collect.ImmutableList;
024    import java.lang.reflect.Constructor;
025    import java.lang.reflect.Field;
026    import java.lang.reflect.InvocationTargetException;
027    import java.lang.reflect.Method;
028    import java.util.ArrayList;
029    import java.util.Collections;
030    import java.util.Comparator;
031    import java.util.List;
032    import java.util.NoSuchElementException;
033    import java.util.ServiceLoader;
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    
037    /**
038     * Utility methods for parameter discovery.
039     *
040     * <p>Please note that this class is marked with {@link Beta} annotation, so it can be subject of incompatible changes
041     * or removal in later releases.</p>
042     *
043     * @author Gabor Imre
044     */
045    
046    @Beta
047    @PublicAPI
048    public final class Discovery {
049    
050        /**
051         * Logger to use.
052         */
053        private static final Log LOG = LogFactory.getLog(Discovery.class);
054    
055        /**
056         * No constructor exposed for utility classes.
057         */
058        private Discovery() {}
059    
060        /**
061         * Retrieve implementations for an interface.
062         *
063         * <p>Note that this implementation restricts DI implementation to be used in Descriptors API discovery. Current
064         * implementation depends on Java standard {@link ServiceLoader} facility, while in the future we might choose a
065         * more advanced tool
066         *
067         * @param <T>   Type to enumerate
068         * @param type  Type to enumerate
069         * @throws NoSuchElementException when no service found
070         * @return  Objects created with default settings for the specific type
071         */
072        @Beta
073        private static <T> List<InstanceWrapper<T>> listServices(final Class<T> type) {
074            final ServiceLoader<T> loader = ServiceLoader.<T>load(type);
075            final List<InstanceWrapper<T>> ret = new ArrayList<InstanceWrapper<T>>();
076            for (T t : loader) {
077                ret.add(new InstanceWrapper(t));
078            }
079            if (ret.isEmpty()) {
080                throw new NoSuchElementException("No implementation found for " + type.getName());
081            }
082            return ImmutableList.copyOf(ret);
083        }
084    
085        /**
086         * Collect instances of a specific type.
087         *
088         * @param <T>   Type to collect
089         * @param type  Type to collect
090         * @return      One or more instances of the specified type. Interfaces are retrieved as services; classes are
091         *              instantiated with their nullary constructors; enums are enumerated
092         */
093        @Beta
094        public static <T> List<InstanceWrapper<T>> listInstances(final Class<T> type) {
095            if (type.isInterface()) {
096                // it is an interface; use service loader to access
097                return listServices(type);
098            } else if (type.isEnum()) {
099                final T [] consts = type.getEnumConstants();
100                if (consts == null || consts.length == 0) {
101                    throw new IllegalArgumentException("Enum type " + type + " does not declare constants");
102                }
103                final List<InstanceWrapper<T>> ret = new ArrayList<InstanceWrapper<T>>();
104                for (T t : consts) {
105                    ret.add(new InstanceWrapper<T>(t));
106                }
107    
108                return ImmutableList.copyOf(ret);
109            } else {
110                // try nullary constructor
111                final Constructor<T> c;
112                try {
113                    c = type.getConstructor();
114                } catch (NoSuchMethodException e) {
115                    throw new IllegalArgumentException("Nullary constructor not found for " + type, e);
116                }
117    
118                try {
119                    final T t = c.newInstance();
120                    return ImmutableList.of(new InstanceWrapper<T>(t));
121                } catch (InstantiationException e) {
122                    throw new IllegalArgumentException("Error invoking nullary constructor of " + type, e);
123                } catch (IllegalAccessException e) {
124                    throw new IllegalArgumentException("Error invoking nullary constructor of " + type, e);
125                } catch (InvocationTargetException e) {
126                    throw new IllegalArgumentException("Error invoking nullary constructor of " + type, e);
127                }
128            }
129        }
130    
131    
132    
133    
134        /**
135         * Look up an instance by its short name.
136         *
137         * @param type      Type to collect (by {@link #listInstances(java.lang.Class)}
138         * @param shortName Short name of the instance to select
139         * @param <T>       Type to collect
140         * @return          The first matching instance if found
141         */
142        public static <T> Optional<InstanceWrapper<T>> selectInstance(final Class<T> type, final String shortName) {
143            // Ensure that passed short name is not null
144            Preconditions.checkNotNull(shortName);
145    
146            // List all instances
147            final List<InstanceWrapper<T>> instances = Discovery.listInstances(type);
148    
149            // Look up matching instance
150            for (InstanceWrapper<T> instance : instances) {
151                if (shortName.equals(instance.getShortName())) {
152                    // Found, return current instance
153                    return Optional.of(instance);
154                }
155            }
156    
157            // Instance not found, return absent
158            return Optional.<InstanceWrapper<T>>absent();
159        }
160    
161    
162        /**
163         * Check if an object is parametrized.
164         *
165         * @param o Object in question
166         * @return  true if o is parametrized
167         */
168        @Beta
169        public static boolean isParametrized(final Object o) {
170            return o.getClass().isAnnotationPresent(Parametrized.class);
171        }
172    
173        /**
174         * Retrieve parameters for a mutable parameter object.
175         *
176         * @param o     Mutable parametrized object annotated with {@link Parametrized}
177         * @throws      IllegalArgumentException when o is not annotated with {@link Parametrized} or no parameters found
178         * @return      Parameters of o wrapped
179         */
180        @Beta
181        public static List<ParameterWrapper> listParameters(final Object o) {
182    
183            if (!o.getClass().isAnnotationPresent(Parametrized.class)) {
184                throw new IllegalArgumentException("Parameter type " + o.getClass().getName() + " is not parametrized");
185            }
186    
187            final ArrayList<ParameterWrapper> ret = new ArrayList<ParameterWrapper>();
188    
189            // see http://stackoverflow.com/questions/1042798/retrieving-the-inherited-attribute-names-values-using-java-reflection
190            for (Class c = o.getClass(); c != null; c = c.getSuperclass()) {
191                final Field [] fields = c.getDeclaredFields();
192                for (Field f : fields) {
193                    if (f.isAnnotationPresent(Parameter.class)) {
194                        final ParameterWrapper w = new ParameterWrapper(o, f);
195                        ret.add(w);
196                    }
197                }
198            }
199    
200            if (ret.isEmpty()) {
201                throw new IllegalArgumentException("No parameters found in " + o.getClass());
202            }
203    
204            Collections.sort(ret, new Comparator<ParameterWrapper>() {
205    
206                public int compare(ParameterWrapper o1, ParameterWrapper o2) {
207                    return o1.getOrder() - o2.getOrder();
208                }
209    
210            });
211    
212            return ImmutableList.<ParameterWrapper>copyOf(ret);
213        }
214    
215    
216    
217        /**
218         * Retrieve the description annotation on an object.
219         * @param o     Object to examine
220         * @throws IllegalArgumentException when no {@link Description} annotation found
221         * @return      The description annotation if exists or null; If o is an instance of an enum then enum constant
222         *              annotation is retrieved
223         */
224        @Beta
225        public static Description getDescriptionAnnotation(final Object o) {
226    
227            if (Enum.class.isAssignableFrom(o.getClass())) {
228                try {
229                    //System.err.println("enum");
230                    // enum constants - which are fields - are annotated, so do some tricks
231                    // see http://stackoverflow.com/questions/7254126/get-annotations-for-enum-type-variable
232                    final Description desc = o.getClass().getField(((Enum) o).name()).getAnnotation(Description.class);
233                    if (desc == null) {
234                        throw new IllegalArgumentException(
235                                "No description found for " + o + " (" + o.getClass().getName() + ")");
236                    }
237                    return desc;
238                } catch (Exception e) {
239                    throw new IllegalArgumentException("Internal error", e);
240                }
241            } else {
242                //System.err.println("no enum "+o+" "+o.getClass());
243                final Description desc = o.getClass().getAnnotation(Description.class);
244                if (desc == null) {
245                    throw new IllegalArgumentException(
246                            "No description found for " + o + " (" + o.getClass().getName() + ")");
247                }
248                return desc;
249            }
250        }
251    
252        /**
253         * CreatorWrapper acts as a type-dependent service loader.
254         *
255         * <p>Note that the order of the returned list is defined by
256         *
257         * @param targetType type to be created
258         * @param o     Instance on which the creators should be called
259         * @param <T>   type to be created
260         * @return      All distinct creator parameters
261         */
262        @Beta
263        public static <T> List<CreatorWrapper<T>> listCreators(final Object o, final Class<T> targetType) {
264            if (LOG.isDebugEnabled()) {
265                LOG.debug("Construct for source " + o.toString() + " with target type " + targetType.getName());
266            }
267    
268            final List<CreatorWrapper<T>> ret = new ArrayList<CreatorWrapper<T>>();
269    
270            // go through all public methods and look for Creator annotation
271            final Method [] methods = o.getClass().getMethods();
272            for (Method m : methods) {
273    
274                if (LOG.isTraceEnabled()) {
275                    LOG.trace("Checking method " + m.toString());
276                }
277    
278                if (!m.isAnnotationPresent(Creator.class)) {
279                    if (LOG.isTraceEnabled()) {
280                        LOG.trace("  Creator annotation not present");
281                    }
282                    continue;
283                }
284    
285                if (!targetType.isAssignableFrom(m.getReturnType())) {
286                    if (LOG.isTraceEnabled()) {
287                        LOG.trace("  Return type is not compatible; skip");
288                    }
289                    continue;
290                }
291                final Class<?> [] params = m.getParameterTypes();
292                if (params.length != 1) {
293                    if (LOG.isTraceEnabled()) {
294                        LOG.trace("  Param count is " + params.length + "; expected 1; skip");
295                    }
296                    continue;
297                }
298    
299                // Parameter type
300                final Class p = params[ 0 ]; // TODO: should be Class<?> ?
301    
302                final List<InstanceWrapper<?>> i = Discovery.listInstances(p);
303    
304                for (InstanceWrapper ccw : i) {
305                    ret.add(new CreatorWrapper<T>(o, m, ccw.get()));
306                }
307            }
308    
309            Collections.sort(ret, new Comparator<CreatorWrapper<T>>() {
310    
311                public int compare(CreatorWrapper<T> o1, CreatorWrapper<T> o2) {
312                    return o1.getOrder() - o2.getOrder();
313                }
314    
315            });
316    
317            return ImmutableList.copyOf(ret);
318        }
319    
320        /**
321         * Create initial Builder if possible.
322         *
323         * <p>Builder association is identified by annotation BuilderClass</p>
324         *
325         * @param   o   An object
326         * @param   <T> Parameter object type
327         * @return  An associated builder instance if the parameter object has associated builder.
328         */
329        @Beta
330        public static <T> Optional<ParameterBuilder<T>> initializeBuilderIfExists(T o) {
331            if (LOG.isTraceEnabled()) {
332                LOG.trace("Look up associated builder for " + o + " (" + o.getClass().getName() + ")");
333            }
334            // Retrieve ant instantiate builder if exists
335            if (o.getClass().isAnnotationPresent(BuilderClass.class)) {
336                if (LOG.isTraceEnabled()) {
337                    LOG.trace("Associated builder class exists.");
338                }
339    
340                // builder is present
341                final BuilderClass builderClass = o.getClass().getAnnotation(BuilderClass.class);
342    
343                if (LOG.isTraceEnabled()) {
344                    LOG.trace("Builder class: " + builderClass.builderClass().getName());
345                }
346    
347                final Class<ParameterBuilder<T>> c = (Class<ParameterBuilder<T>>) builderClass.builderClass();
348    
349                if (LOG.isTraceEnabled()) {
350                    LOG.trace("Instantiate builder class");
351                }
352    
353                final ParameterBuilder<T> b;
354                try {
355                    b = c.getConstructor(o.getClass()).newInstance(o);
356                } catch (InstantiationException e) {
357                    throw new IllegalArgumentException(e);
358                } catch (InvocationTargetException e) {
359                    throw new IllegalArgumentException(e);
360                } catch (IllegalAccessException e) {
361                    throw new IllegalArgumentException(e);
362                } catch (NoSuchMethodException e) {
363                    throw new IllegalArgumentException(e);
364                }
365    
366                return Optional.of(b);
367    
368            } else {
369                if (LOG.isTraceEnabled()) {
370                    LOG.trace("No associated builder exists.");
371                }
372                return Optional.<ParameterBuilder<T>>absent();
373            }
374        }
375    }