/******************************************************************************* * Copyright 2014 Analog Devices, Inc. * * 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 com.analog.lyric.collect; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import com.google.common.collect.ArrayListMultimap; import com.google.common.reflect.ClassPath; import net.jcip.annotations.GuardedBy; import net.jcip.annotations.ThreadSafe; /** * Simple registry mapping class names to constructor instances. * <p> * Note that {@link Map} methods such as {@link #containsKey}, {@link #containsValue}, * {@link #keySet}, {@link #values}, {@link #entrySet}, and {@link #size} will only reflect keys * that have been explicitly looked up using one of {@link #get}, {@link #getClass}, * {@link #instantiate} or {@link #loadAll()}. * <p> * * @param <T> is the super class of all of the classes in the registry. * @since 0.07 * @author Christopher Barber */ @ThreadSafe public class ConstructorRegistry<T> extends AbstractMap<String, Constructor<T>> { private final Class<? super T> _superClass; /** * The number of packages specified in the constructor. */ private final int _nInitialPackages; @GuardedBy("this") protected final ArrayList<String> _packages; @GuardedBy("this") protected final ArrayListMultimap<String, Constructor<T>> _nameToConstructors; /*-------------- * Construction */ /** * Constructs a new registry instance for given super class. * <p> * The registry will automatically search for classes in the same package in which * {@code superClass} is declared. Additional packages may be added using {@link #addPackage}. * <p> * * @param superClass is the runtime class type corresponding to the declared parameter type * {@code T}. * @since 0.07 */ public ConstructorRegistry(Class<? super T> superClass) { this(superClass, new String[] { superClass.getPackage().getName() }); } /** * Constructs a new registry instance for given super class. * <p> * * @param superClass is the runtime class type corresponding to the declared parameter type * {@code T}. * @param packages are the packages in which to search for subclass implementations. * @since 0.07 */ public ConstructorRegistry(Class<? super T> superClass, String... packages) { _superClass = superClass; _nInitialPackages = packages.length; _packages = new ArrayList<String>(); for (String packageName : packages) { _packages.add(packageName); } _nameToConstructors = ArrayListMultimap.create(); } /*------------- * Map methods */ @NonNullByDefault(false) @Override public boolean containsKey(Object simpleClassName) { return _nameToConstructors.containsKey(simpleClassName); } @Override @NonNullByDefault(false) public boolean containsValue(Object value) { return _nameToConstructors.containsValue(value); } @Override public Set<Map.Entry<String, Constructor<T>>> entrySet() { return Collections.unmodifiableSet(new HashSet<>(_nameToConstructors.entries())); } /** * Looks up no-argument constructor for named class. * <p> * If {@code className} is a fully qualified name referring to a class with an accessible constructor * that takes no arguments, that constructor will be returned. Otherwise, this will * searches all of the registry's packages (see {@link #getPackages()}) for class with given * {@code className} and compatible constructor. Returns first match or null if none is found. * <p> * @param className is either the unqualified or fully qualified name of the class whose constructor is sought. * @see #get(String, Class[]) */ @NonNullByDefault(false) @Override public final @Nullable Constructor<T> get(Object className) { if (className instanceof String) { return get((String)className, ArrayUtil.EMPTY_CLASS_ARRAY); } return null; } @Override public Set<String> keySet() { return Collections.unmodifiableSet(_nameToConstructors.keySet()); } @Override public int size() { return _nameToConstructors.size(); } @Override public Collection<Constructor<T>> values() { return Collections.unmodifiableCollection(_nameToConstructors.values()); } /*--------------- * Local methods */ /** * Adds entry for specified class. * <p> * * @param newClass * @throws IllegalArgumentException if class is not a subclass of registry's * {@linkplain #getSuperClass() super class} or if it does not have a publicly * accessible constructor that takes no arguments. * @since 0.07 * @see #addPackage */ public void addClass(Class<?> newClass) { final String name = newClass.getSimpleName(); if (!_superClass.isAssignableFrom(newClass)) { throw new IllegalArgumentException(String.format("%s is not a subclass of %s", name, _superClass.getSimpleName())); } if (addConstructorsFrom(newClass, true).isEmpty()) { throw new IllegalArgumentException(String.format("%s does not have an accessible constructor", name)); } } /** * Adds a package to search for class implementations. * <p> * * @param packageName is a fully qualified Java package name expected to contain subclasses of * declared superclass {@code T}. * @see #get(Object) * @since 0.07 * @see #addClass * @see #getPackages() */ public synchronized void addPackage(String packageName) { _packages.add(packageName); } /** * Looks up constructor for named class with specified formal parameters. * <p> * If {@code className} is a fully qualified name referring to a class with an accessible constructor * that takes no arguments, that constructor will be returned. Otherwise, this will * searches all of the registry's packages (see {@link #getPackages()}) for class with given * {@code className} and compatible constructor. Returns first match or null if none is found. * <p> * @param className is either the unqualified or fully qualified name of the class whose constructor is sought. * @param formalParameters are the declared types of the parameters to the constructor. Note that these must match * exactly. * @see #get(String, Class[]) */ public final @Nullable Constructor<T> get(String className, Class<?>[] formalParameters) { List<Constructor<T>> constructors = getAll(className); for (Constructor<T> constructor : constructors) { if (Arrays.equals(formalParameters, constructor.getParameterTypes())) { return constructor; } } return null; } /** * Returns list of all public constructors for class with given name. * @param className is either a simple or fully qualified class name. If a simple name and the class * has not already been loaded, this will search {@link #getPackages()} for a match. * @return non-null list of constructors, which may be empty. * @see #get * @since 0.07 */ public List<Constructor<T>> getAll(String className) { String name = className; List<Constructor<T>> constructors = Collections.emptyList(); synchronized(this) { constructors = _nameToConstructors.get(name); } if (constructors.isEmpty()) { ClassLoader loader = getClass().getClassLoader(); if (name.indexOf('.') > 0) { // Looks like it is qualified with a package name. try { constructors = addConstructorsFrom(Class.forName(name, false, loader), false); } catch (Throwable e) { } } // Search packages for a matching class for (String packageName : _packages) { String fullQualifiedName = packageName + "." + name; try { constructors = addConstructorsFrom(Class.forName(fullQualifiedName, false, loader), true); if (!constructors.isEmpty()) { break; } } catch (Throwable e) { } } } return constructors; } /** * Returns class type named by {@code simpleClassName}. * <p> * Simply returns {@linkplain Constructor#getDeclaringClass() declaring class} of value returned * by {@link #get}. * * @throws RuntimeException if no such class can be found. * @since 0.07 */ public Class<? extends T> getClass(String simpleClassName) { Class<? extends T> c = getClassOrNull(simpleClassName); if (c == null) { throw noMatchingClass(simpleClassName); } return c; } /** * Returns class type named by {@code simpleClassName}. * <p> * Simply returns {@linkplain Constructor#getDeclaringClass() declaring class} of value returned * by {@link #get} or else null. * * @since 0.07 */ @Nullable public Class<? extends T> getClassOrNull(String simpleClassName) { List<Constructor<T>> constructors = getAll(simpleClassName); if (!constructors.isEmpty()) { return constructors.get(0).getDeclaringClass(); } return null; } /** * Returns copy of list of package names searched by this registry. * <p> * * @since 0.07 * @see #addPackage(String) * @see #get(Object) */ public synchronized String[] getPackages() { return _packages.toArray(new String[_packages.size()]); } /** * Class type of declared type {@code T}. * * @since 0.07 */ public Class<? super T> getSuperClass() { return _superClass; } /** * Instantiates an instance of named class using no-argument constructor. * <p> * Simply invokes {@link Constructor#newInstance} on constructor returned by {@link #get}. * * @throws RuntimeException if no such class can be found. * @since 0.07 */ public T instantiate(String simpleClassName) { T instance = instantiateOrNull(simpleClassName); if (instance == null) { throw noMatchingClass(simpleClassName); } return instance; } /** * Instantiates an instance of named class using no-argument constructor. * <p> * Simply invokes {@link Constructor#newInstance} on constructor returned by {@link #get} or * else returns null. * * @since 0.07 */ @Nullable public T instantiateOrNull(String simpleClassName) { Constructor<T> constructor = get(simpleClassName); if (constructor != null) { try { return constructor.newInstance(); } catch (InvocationTargetException ex) { throw new RuntimeException(ex.getCause()); } catch (ReflectiveOperationException ex) { throw new RuntimeException(ex); } } return null; } /** * Preloads all subclasses of declared superclass {@code T} found in registry's packages. * <p> * Searches all of the packages in {@link #getPackages()} for subclasses of {@code T} and adds * then to the registry. * <p> * * @since 0.07 */ public synchronized void loadAll() { ClassLoader loader = getClass().getClassLoader(); ClassPath path; try { path = ClassPath.from(loader); } catch (IOException ex) { throw new RuntimeException(ex); } for (String packageName : _packages) { for (ClassPath.ClassInfo info : path.getTopLevelClasses(packageName)) { addConstructorsFrom(info.load(), true); } } } /** * Resets back to initial state upon construction. * <p> * @since 0.08 */ public synchronized void reset() { // Remove any added packages _packages.subList(_nInitialPackages, _packages.size()).clear(); // Clear constructor cache _nameToConstructors.clear(); } /*----------------- * Private methods */ @NonNullByDefault(false) private static enum ConstructorComparator implements Comparator<Constructor<?>> { INSTANCE; @Override public int compare(Constructor<?> c1, Constructor<?> c2) { return Integer.compare(c1.getParameterTypes().length, c2.getParameterTypes().length); } } /** * Adds public constructors from given type indexed by qualified name * @param addSimpleName if true, then constructors will also be indexed by the classes simple name. * @return the constructors that were added * @since 0.07 */ @SuppressWarnings("unchecked") private synchronized List<Constructor<T>> addConstructorsFrom(Class<?> type, boolean addSimpleName) { List<Constructor<T>> constructors = Collections.emptyList(); int modifiers = type.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers) && _superClass.isAssignableFrom(type)) { try { final Constructor<T>[] array = (Constructor<T>[]) type.getConstructors(); Arrays.sort(array, ConstructorComparator.INSTANCE); constructors = Arrays.asList(array); if (addSimpleName) { _nameToConstructors.putAll(type.getSimpleName(), constructors); } _nameToConstructors.putAll(type.getCanonicalName(), constructors); } catch (SecurityException ex) { // Ignore } } return constructors; } protected RuntimeException noMatchingClass(String simpleClassName) { return new RuntimeException(String.format( "Cannot find class named '%s' with accessible constructor with appropriate signature", simpleClassName)); } }