com.analog.lyric.collect.ConstructorRegistry.java Source code

Java tutorial

Introduction

Here is the source code for com.analog.lyric.collect.ConstructorRegistry.java

Source

/*******************************************************************************
 *   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));
    }

}