/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.lucene.util; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.LinkedHashMap; import java.util.Set; import java.util.ServiceConfigurationError; /** * Helper class for loading named SPIs from classpath (e.g. Codec, PostingsFormat). * @lucene.internal */ public final class NamedSPILoader<S extends NamedSPILoader.NamedSPI> implements Iterable<S> { private volatile Map<String,S> services = Collections.emptyMap(); private final Class<S> clazz; public NamedSPILoader(Class<S> clazz) { this(clazz, Thread.currentThread().getContextClassLoader()); } public NamedSPILoader(Class<S> clazz, ClassLoader classloader) { this.clazz = clazz; // if clazz' classloader is not a parent of the given one, we scan clazz's classloader, too: final ClassLoader clazzClassloader = clazz.getClassLoader(); if (classloader == null) { classloader = clazzClassloader; } if (clazzClassloader != null && !SPIClassIterator.isParentClassLoader(clazzClassloader, classloader)) { reload(clazzClassloader); } reload(classloader); } /** * Reloads the internal SPI list from the given {@link ClassLoader}. * Changes to the service list are visible after the method ends, all * iterators ({@link #iterator()},...) stay consistent. * * <p><b>NOTE:</b> Only new service providers are added, existing ones are * never removed or replaced. * * <p><em>This method is expensive and should only be called for discovery * of new service providers on the given classpath/classloader!</em> */ public void reload(ClassLoader classloader) { Objects.requireNonNull(classloader, "classloader"); final LinkedHashMap<String,S> services = new LinkedHashMap<>(this.services); final SPIClassIterator<S> loader = SPIClassIterator.get(clazz, classloader); while (loader.hasNext()) { final Class<? extends S> c = loader.next(); try { final S service = c.newInstance(); final String name = service.getName(); // only add the first one for each name, later services will be ignored // this allows to place services before others in classpath to make // them used instead of others if (!services.containsKey(name)) { checkServiceName(name); services.put(name, service); } } catch (Exception e) { throw new ServiceConfigurationError("Cannot instantiate SPI class: " + c.getName(), e); } } this.services = Collections.unmodifiableMap(services); } /** * Validates that a service name meets the requirements of {@link NamedSPI} */ public static void checkServiceName(String name) { // based on harmony charset.java if (name.length() >= 128) { throw new IllegalArgumentException("Illegal service name: '" + name + "' is too long (must be < 128 chars)."); } for (int i = 0, len = name.length(); i < len; i++) { char c = name.charAt(i); if (!isLetterOrDigit(c)) { throw new IllegalArgumentException("Illegal service name: '" + name + "' must be simple ascii alphanumeric."); } } } /** * Checks whether a character is a letter or digit (ascii) which are defined in the spec. */ private static boolean isLetterOrDigit(char c) { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9'); } public S lookup(String name) { final S service = services.get(name); if (service != null) return service; throw new IllegalArgumentException("An SPI class of type "+clazz.getName()+" with name '"+name+"' does not exist."+ " You need to add the corresponding JAR file supporting this SPI to your classpath."+ " The current classpath supports the following names: "+availableServices()); } public Set<String> availableServices() { return services.keySet(); } @Override public Iterator<S> iterator() { return services.values().iterator(); } /** * Interface to support {@link NamedSPILoader#lookup(String)} by name. * <p> * Names must be all ascii alphanumeric, and less than 128 characters in length. */ public static interface NamedSPI { String getName(); } }