
org.apache.lucene.analysis.AnalysisSPILoader Maven / Gradle / Ivy
Show all versions of org.apache.servicemix.bundles.lucene
/*
* 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.analysis;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.lucene.util.ClassLoaderUtils;
/**
* Helper class for loading named SPIs from classpath (e.g. Tokenizers, TokenStreams).
*
* @lucene.internal
*/
public final class AnalysisSPILoader {
private volatile Map> services = Collections.emptyMap();
private volatile Set originalNames = Collections.emptySet();
private final Class clazz;
private static final Pattern SERVICE_NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+$");
public AnalysisSPILoader(Class clazz) {
this(clazz, null);
}
public AnalysisSPILoader(Class 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
&& !ClassLoaderUtils.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 (e.g., from {@link #availableServices()},...)
* stay consistent.
*
* NOTE: Only new service providers are added, existing ones are never removed or
* replaced.
*
*
This method is expensive and should only be called for discovery of new service
* providers on the given classpath/classloader!
*/
public synchronized void reload(ClassLoader classloader) {
Objects.requireNonNull(classloader, "classloader");
final LinkedHashMap> services = new LinkedHashMap<>(this.services);
final LinkedHashSet originalNames = new LinkedHashSet<>(this.originalNames);
ServiceLoader.load(clazz, classloader).stream()
.map(ServiceLoader.Provider::type)
.forEachOrdered(
service -> {
String name = null;
String originalName = null;
Throwable cause = null;
try {
originalName = lookupSPIName(service);
name = originalName.toLowerCase(Locale.ROOT);
if (!isValidName(originalName)) {
throw new ServiceConfigurationError(
"The name "
+ originalName
+ " for "
+ service.getName()
+ " is invalid: Allowed characters are (English) alphabet, digits, and underscore. It should be started with an alphabet.");
}
} catch (NoSuchFieldException | IllegalAccessException | IllegalStateException e) {
cause = e;
}
if (name == null) {
throw new ServiceConfigurationError(
"The class name "
+ service.getName()
+ " has no service name field: [public static final String NAME]",
cause);
}
// 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
//
// TODO: Should we disallow duplicate names here?
// Allowing it may get confusing on collisions, as different packages
// could contain same factory class, which is a naming bug!
// When changing this be careful to allow reload()!
if (!services.containsKey(name)) {
services.put(name, service);
// preserve (case-sensitive) original name for reference
originalNames.add(originalName);
}
});
// make sure that the number of lookup keys is same to the number of original names.
// in fact this constraint should be met in existence checks of the lookup map key,
// so this is more like an assertion rather than a status check.
if (services.keySet().size() != originalNames.size()) {
throw new ServiceConfigurationError(
"Service lookup key set is inconsistent with original name set!");
}
this.services = Map.copyOf(services);
this.originalNames = Set.copyOf(originalNames);
}
private boolean isValidName(String name) {
return SERVICE_NAME_PATTERN.matcher(name).matches();
}
public S newInstance(String name, Map args) {
final Class extends S> service = lookupClass(name);
return newFactoryClassInstance(service, args);
}
public Class extends S> lookupClass(String name) {
final Class extends S> service = services.get(name.toLowerCase(Locale.ROOT));
if (service != null) {
return service;
} else {
throw new IllegalArgumentException(
"A 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 availableServices() {
return originalNames;
}
/**
* Looks up SPI name (static "NAME" field) with appropriate modifiers. Also it must be a String
* class and declared in the concrete class.
*
* @return the SPI name
* @throws NoSuchFieldException - if the "NAME" field is not defined.
* @throws IllegalAccessException - if the "NAME" field is inaccessible.
* @throws IllegalStateException - if the "NAME" field does not have appropriate modifiers or
* isn't a String field.
*/
public static String lookupSPIName(Class extends AbstractAnalysisFactory> service)
throws NoSuchFieldException, IllegalAccessException, IllegalStateException {
final Field field = service.getField("NAME");
int modifier = field.getModifiers();
if (Modifier.isStatic(modifier)
&& Modifier.isFinal(modifier)
&& Objects.equals(field.getDeclaringClass(), service)
&& Objects.equals(field.getType(), String.class)) {
return ((String) field.get(null));
}
throw new IllegalStateException("No SPI name defined.");
}
/**
* Creates a new instance of the given {@link AbstractAnalysisFactory} by invoking the
* constructor, passing the given argument map.
*/
public static T newFactoryClassInstance(
Class clazz, Map args) {
try {
return clazz.getConstructor(Map.class).newInstance(args);
} catch (InvocationTargetException ite) {
final Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException(
"Unexpected checked exception while calling constructor of " + clazz.getName(), cause);
} catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException(
"Factory "
+ clazz.getName()
+ " cannot be instantiated. This is likely due to missing Map constructor.",
e);
}
}
}