/* * 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 supérieure 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; } }