ServiceLoader.java
/*
* 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.tika.config;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.utils.ServiceLoaderUtils;
/**
* Internal utility class that Tika uses to look up service providers.
*
* @since Apache Tika 0.9
*/
public class ServiceLoader {
/**
* The dynamic set of services available in an OSGi environment.
* Managed by the {@link TikaActivator} class and used as an additional
* source of service instances in the {@link #loadServiceProviders(Class)}
* method.
*/
private static final Map<Object, RankedService> SERVICES = new HashMap<>();
private static final Pattern COMMENT = Pattern.compile("#.*");
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
/**
* The default context class loader to use for all threads, or
* <code>null</code> to automatically select the context class loader.
*/
private static volatile ClassLoader CONTEXT_CLASS_LOADER = null;
private final ClassLoader loader;
private final boolean dynamic;
public ServiceLoader(ClassLoader loader, boolean dynamic) {
this.loader = loader;
this.dynamic = dynamic;
}
public ServiceLoader(ClassLoader loader) {
this(loader, true);
}
public ServiceLoader() {
this(getContextClassLoader(), true);
}
/**
* Returns the context class loader of the current thread. If such
* a class loader is not available, then the loader of this class or
* finally the system class loader is returned.
*
* @return context class loader, or <code>null</code> if no loader
* is available
* @see <a href="https://issues.apache.org/jira/browse/TIKA-441">TIKA-441</a>
*/
static ClassLoader getContextClassLoader() {
ClassLoader loader = CONTEXT_CLASS_LOADER;
if (loader == null) {
loader = ServiceLoader.class.getClassLoader();
}
if (loader == null) {
loader = ClassLoader.getSystemClassLoader();
}
return loader;
}
/**
* Sets the context class loader to use for all threads that access
* this class. Used for example in an OSGi environment to avoid problems
* with the default context class loader.
*
* @param loader default context class loader,
* or <code>null</code> to automatically pick the loader
*/
public static void setContextClassLoader(ClassLoader loader) {
CONTEXT_CLASS_LOADER = loader;
}
static void addService(Object reference, Object service, int rank) {
synchronized (SERVICES) {
SERVICES.put(reference, new RankedService(service, rank));
}
}
static Object removeService(Object reference) {
synchronized (SERVICES) {
return SERVICES.remove(reference);
}
}
/**
* Returns if the service loader is static or dynamic
*
* @return dynamic or static loading
* @since Apache Tika 1.10
*/
public boolean isDynamic() {
return dynamic;
}
/**
* Returns an input stream for reading the specified resource from the
* configured class loader.
*
* @param name resource name
* @return input stream, or <code>null</code> if the resource was not found
* @see ClassLoader#getResourceAsStream(String)
* @since Apache Tika 1.1
*/
public InputStream getResourceAsStream(String name) {
if (loader != null) {
return loader.getResourceAsStream(name);
} else {
return null;
}
}
/**
* @return ClassLoader used by this ServiceLoader
* @see #getContextClassLoader() for the context's ClassLoader
* @since Apache Tika 1.15.1
*/
public ClassLoader getLoader() {
return loader;
}
/**
* Loads and returns the named service class that's expected to implement
* the given interface.
* <p>
*
* @param iface service interface
* @param name service class name
* @return service class
* @throws ClassNotFoundException if the service class can not be found
* or does not implement the given interface
* @see Class#forName(String, boolean, ClassLoader)
* @since Apache Tika 1.1
*/
@SuppressWarnings("unchecked")
public <T> Class<? extends T> getServiceClass(Class<T> iface, String name)
throws ClassNotFoundException {
if (loader == null) {
throw new ClassNotFoundException("Service class " + name + " is not available");
}
Class<?> klass = Class.forName(name, true, loader);
if (klass.isInterface()) {
throw new ClassNotFoundException("Service class " + name + " is an interface");
} else if (!iface.isAssignableFrom(klass)) {
throw new ClassNotFoundException(
"Service class " + name + " does not implement " + iface.getName());
} else {
return (Class<? extends T>) klass;
}
}
/**
* Returns all the available service resources matching the
* given pattern, such as all instances of tika-mimetypes.xml
* on the classpath, or all org.apache.tika.parser.Parser
* service files.
*/
public Enumeration<URL> findServiceResources(String filePattern) {
try {
return loader.getResources(filePattern);
} catch (IOException ignore) {
// We couldn't get the list of service resource files
List<URL> empty = Collections.emptyList();
return Collections.enumeration(empty);
}
}
/**
* Returns all the available service providers of the given type.
*
* As of versions after 2.4.1, this removes duplicate classes
*
* @param iface service provider interface
* @return available service providers
*/
public <T> List<T> loadServiceProviders(Class<T> iface) {
List<T> tmp = new ArrayList<>();
tmp.addAll(loadDynamicServiceProviders(iface));
tmp.addAll(loadStaticServiceProviders(iface));
List<T> providers = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (T provider : tmp) {
if (! seen.contains(provider.getClass().getCanonicalName())) {
providers.add(provider);
seen.add(provider.getClass().getCanonicalName());
}
}
return providers;
}
/**
* Returns the available dynamic service providers of the given type.
* The returned list is newly allocated and may be freely modified
* by the caller.
*
* @param iface service provider interface
* @return dynamic service providers
* @since Apache Tika 1.2
*/
@SuppressWarnings("unchecked")
public <T> List<T> loadDynamicServiceProviders(Class<T> iface) {
if (dynamic) {
synchronized (SERVICES) {
List<RankedService> list = new ArrayList<>(SERVICES.values());
Collections.sort(list);
List<T> providers = new ArrayList<>(list.size());
for (RankedService service : list) {
if (service.isInstanceOf(iface)) {
providers.add((T) service.service);
}
}
return providers;
}
} else {
return Collections.EMPTY_LIST;
}
}
/**
* Returns the defined static service providers of the given type, without
* attempting to load them.
* The providers are loaded using the service provider mechanism using
* the configured class loader (if any).
*
* @param iface service provider interface
* @return static list of uninitialised service providers
* @since Apache Tika 1.6
*/
protected <T> List<String> identifyStaticServiceProviders(Class<T> iface) {
List<String> names = new ArrayList<>();
if (loader != null) {
String serviceName = iface.getName();
Enumeration<URL> resources = findServiceResources("META-INF/services/" + serviceName);
for (URL resource : Collections.list(resources)) {
try {
collectServiceClassNames(resource, names);
} catch (IOException e) {
//TODO -- swallow log? or don't catch?
}
}
}
return names;
}
public <T> List<T> loadStaticServiceProviders(Class<T> iface) {
return loadStaticServiceProviders(iface, Collections.EMPTY_SET);
}
/**
* Returns the available static service providers of the given type.
* The providers are loaded using the service provider mechanism using
* the configured class loader (if any). The returned list is newly
* allocated and may be freely modified by the caller.
*
* @param iface service provider interface
* @param excludes -- do not load these classes
* @return static service providers
* @since Apache Tika 1.2
*/
@SuppressWarnings("unchecked")
public <T> List<T> loadStaticServiceProviders(Class<T> iface,
Collection<Class<? extends T>> excludes) {
List<T> providers = new ArrayList<>();
if (loader != null) {
List<String> names = identifyStaticServiceProviders(iface);
for (String name : names) {
try {
Class<?> klass = loader.loadClass(name);
if (iface.isAssignableFrom(klass)) {
boolean shouldExclude = false;
for (Class<? extends T> ex : excludes) {
if (ex.isAssignableFrom(klass)) {
shouldExclude = true;
break;
}
}
if (!shouldExclude) {
T instance = ServiceLoaderUtils.newInstance(klass, this);
providers.add(instance);
}
} else {
throw new TikaConfigException(
"Class " + name + " is not of type: " + iface);
}
} catch (Throwable t) {
//TODO: swallow, log, throw?
}
}
}
return providers;
}
private void collectServiceClassNames(URL resource, Collection<String> names)
throws IOException {
try (InputStream stream = resource.openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8))) {
String line = reader.readLine();
while (line != null) {
line = COMMENT.matcher(line).replaceFirst("");
line = WHITESPACE.matcher(line).replaceAll("");
if (line.length() > 0) {
names.add(line);
}
line = reader.readLine();
}
}
}
private static class RankedService implements Comparable<RankedService> {
private final Object service;
private final int rank;
public RankedService(Object service, int rank) {
this.service = service;
this.rank = rank;
}
public boolean isInstanceOf(Class<?> iface) {
return iface.isAssignableFrom(service.getClass());
}
public int compareTo(RankedService that) {
return that.rank - rank; // highest number first
}
}
}