fr.ens.biologie.genomique.eoulsan.util.ServiceNameLoader.java Source code

Java tutorial

Introduction

Here is the source code for fr.ens.biologie.genomique.eoulsan.util.ServiceNameLoader.java

Source

/*
 *                  Eoulsan development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public License version 2.1 or
 * later and CeCILL-C. This should be distributed with the code.
 * If you do not have a copy, see:
 *
 *      http://www.gnu.org/licenses/lgpl-2.1.txt
 *      http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt
 *
 * Copyright for this code is held jointly by the Genomic platform
 * of the Institut de Biologie de l'cole normale suprieure and
 * the individual authors. These should be listed in @author doc
 * comments.
 *
 * For more information on the Eoulsan project and its aims,
 * or to join the Eoulsan Google group, visit the home page
 * at:
 *
 *      http://outils.genomique.biologie.ens.fr/eoulsan
 *
 */

package fr.ens.biologie.genomique.eoulsan.util;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceConfigurationError;
import java.util.Set;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;

/**
 * This class is a service loader that allow to filter class to retrieve and get
 * service by its name and not by its class name.
 * @author Laurent Jourdren
 * @since 2.0
 */
public abstract class ServiceNameLoader<S> {

    public static final String PREFIX = "META-INF/services/";

    private final Class<S> service;
    private final ClassLoader loader;
    private final ListMultimap<String, String> classNames = ArrayListMultimap.create();
    private final ListMultimap<String, S> cache = ArrayListMultimap.create();
    private final Set<String> classesToNotLoad = new HashSet<>();
    private boolean notYetLoaded = true;

    /**
     * This method allow to filter class that can be returned by
     * EoulsanServiceLoader.
     * @param clazz class to test
     * @return true if the class is allowed
     */
    protected abstract boolean accept(Class<?> clazz);

    /**
     * Get the method of the class service to use to get the name of the service.
     * @return a string with the method name
     */
    protected abstract String getMethodName();

    /**
     * Test if results of new instance.
     * @return true if results must be cached
     */
    protected boolean isCache() {

        return false;
    }

    /**
     * Test if service name must be case sensible.
     * @return true if service name must be case sensible
     */
    protected boolean isServiceNameCaseSensible() {

        return false;
    }

    /**
     * Add a class to not load.
     * @param className class name to not load
     */
    public void addClassToNotLoad(final String className) {

        if (className == null) {
            throw new NullPointerException("className argument cannot be null");
        }

        this.classesToNotLoad.add(className.trim());
    }

    /**
     * Add classes to not load.
     * @param classNames class names to not load
     */
    public void addClassesToNotLoad(final Collection<String> classNames) {

        if (classNames == null) {
            throw new NullPointerException("classNames argument cannot be null");
        }

        for (String className : classNames) {
            if (className != null) {
                addClassToNotLoad(className);
            }
        }
    }

    /**
     * Clear classes to not load.
     */
    public void clearClassesToNotLoad() {

        this.classesToNotLoad.clear();
    }

    /**
     * Remove a class to not load.
     * @param className class name
     */
    public void removeClassToNotLoad(final String className) {

        if (className == null) {
            throw new NullPointerException("className argument cannot be null");
        }

        this.classesToNotLoad.remove(className.trim());
    }

    /**
     * Remove classes to not load.
     * @param classNames class names
     */
    public void removeClassesToNotLoad(final Collection<String> classNames) {

        if (classNames == null) {
            throw new NullPointerException("classNames argument cannot be null");
        }

        for (String className : classNames) {
            if (className != null) {
                removeClassToNotLoad(className);
            }
        }
    }

    /**
     * Get the class names to not load.
     * @return a set with the names of the classes to not load
     */
    public Set<String> getClassesToNotLoad() {

        return Collections.unmodifiableSet(this.classesToNotLoad);
    }

    /**
     * Reload the list of the available class services.
     */
    public void reload() {

        this.notYetLoaded = false;
        this.classNames.clear();
        this.cache.clear();

        if (getMethodName() == null) {
            throw new NullPointerException("getMethod() cannot return null");
        }

        try {

            for (ServiceListLoader.Entry e : ServiceListLoader.loadEntries(this.service.getName())) {

                // Check if class is allowed to be load
                if (!this.classesToNotLoad.contains(e.getValue())) {

                    // Process class
                    processClassName(e.getUrl().toString(), e.getLineNumber(), e.getValue());
                }
            }

        } catch (IOException e) {
            throw new ServiceConfigurationError(this.service.getName() + ": " + e.getMessage());
        }

    }

    /**
     * Parse a SPI file.
     * @param url URL of the file
     */
    private void processClassName(final String url, final int lineNumber, final String className) {

        // Check if the class name is valid
        if (!checkClassName(className)) {
            throw new ServiceConfigurationError(
                    this.service.getName() + ": " + url + ":" + lineNumber + ": Invalid Java class name");
        }

        final Class<?> clazz;

        // Check if the class exists
        try {
            clazz = Class.forName(className, false, this.loader);

        } catch (ClassNotFoundException e) {
            throw new ServiceConfigurationError(
                    this.service.getName() + ": " + url + ": Class not found: " + className);
        }

        // Filter classes
        if (!accept(clazz)) {
            return;
        }

        // Check type
        final S obj;
        try {
            obj = this.service.cast(clazz.newInstance());
        } catch (InstantiationException | IllegalAccessException e) {
            throw new ServiceConfigurationError(
                    this.service.getName() + ": " + url + ": Class cannot be instanced: " + className);
        }

        final Method m;
        try {
            m = obj.getClass().getMethod(getMethodName());
        } catch (SecurityException | NoSuchMethodException e) {
            throw new ServiceConfigurationError(this.service.getName() + ": " + url + ": Method " + getMethodName()
                    + "() cannot be instanced in class: " + className);
        }

        final String name;
        try {
            name = (String) m.invoke(obj);
        } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
            throw new ServiceConfigurationError(this.service.getName() + ": " + url + ": Method " + getMethodName()
                    + "() cannot be invoked in class: " + className);
        }

        if (name == null) {
            throw new ServiceConfigurationError(
                    this.service.getName() + ": " + url + ": Method " + getMethodName() + "() returns null");
        }

        final String serviceName = isServiceNameCaseSensible() ? name : name.toLowerCase();

        if (!this.classNames.containsValue(className)) {
            this.classNames.put(serviceName, className);
        }
    }

    /**
     * Check if the class name is a valid java class name
     * @param className class name to test
     * @return true if the class name is valid
     */
    private static boolean checkClassName(final String className) {

        if (className == null) {
            return false;
        }

        final int len = className.length();
        if (len == 0) {
            return false;
        }

        int codePoint = className.codePointAt(0);
        if (!Character.isJavaIdentifierPart(codePoint)) {
            return false;
        }

        for (int i = 1; i < len; i++) {

            codePoint = className.codePointAt(i);
            if (!Character.isJavaIdentifierPart(codePoint) && codePoint != '.') {
                return false;
            }
        }

        return true;
    }

    /**
     * Create a new service from its name.
     * @param serviceName name of the service
     * @return a new object
     */
    public S newService(final String serviceName) {

        final List<S> newServices = newServices(serviceName);

        if (newServices.isEmpty()) {
            return null;
        }

        return newServices.get(0);
    }

    /**
     * Create a list of new services from its name.
     * @param serviceName name of the service
     * @return a list with the new objects
     */
    public List<S> newServices(final String serviceName) {

        if (serviceName == null) {
            return Collections.emptyList();
        }

        if (this.notYetLoaded) {
            reload();
        }

        final String serviceNameLower = isServiceNameCaseSensible() ? serviceName.trim()
                : serviceName.toLowerCase().trim();

        // Test if service is already in cache
        if (this.cache.containsKey(serviceNameLower)) {

            return this.cache.get(serviceNameLower);
        }

        final List<S> result = new ArrayList<>();

        if (this.classNames.containsKey(serviceNameLower)) {

            for (String className : this.classNames.get(serviceNameLower)) {

                try {
                    final Class<?> clazz = Class.forName(className, true, this.loader);

                    final S newInstance = this.service.cast(clazz.newInstance());

                    // Fill cache is needed
                    if (isCache()) {
                        this.cache.put(serviceNameLower, newInstance);
                    }

                    result.add(newInstance);
                } catch (InstantiationException | IllegalAccessException e) {
                    throw new ServiceConfigurationError(
                            this.service.getName() + ": " + serviceNameLower + " cannot be instanced");
                } catch (ClassNotFoundException e) {
                    throw new ServiceConfigurationError(
                            this.service.getName() + ": Class for " + serviceNameLower + " cannot be found");
                }
            }
        }

        return Collections.unmodifiableList(result);
    }

    /**
     * Return the list of the available services.
     * @return a MultiMap with the available services
     */
    public ListMultimap<String, String> getServiceClasses() {

        if (this.notYetLoaded) {
            reload();
        }

        return Multimaps.unmodifiableListMultimap(this.classNames);
    }

    /**
     * Test if a service exists.
     * @param serviceName name of the service
     * @return true if service exists
     */
    public boolean isService(final String serviceName) {

        if (serviceName == null) {
            return false;
        }

        if (this.notYetLoaded) {
            reload();
        }

        final String serviceNameLower = serviceName.toLowerCase().trim();

        return this.classNames.containsKey(serviceNameLower);
    }

    //
    // Constructor
    //

    /**
     * Public constructor.
     * @param service service class
     */
    public ServiceNameLoader(final Class<S> service) {

        this(service, null);
    }

    /**
     * Public constructor.
     * @param service service class
     * @param loader class loader to use
     */
    public ServiceNameLoader(final Class<S> service, final ClassLoader loader) {

        if (service == null) {
            throw new NullPointerException("The service is null");
        }

        this.service = service;
        this.loader = loader == null ? Thread.currentThread().getContextClassLoader() : loader;
    }

}