/*
* 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.kafka.connect.runtime.isolation;
import org.apache.kafka.connect.connector.Connector;
import org.apache.kafka.connect.storage.Converter;
import org.apache.kafka.connect.transforms.Transformation;
import org.reflections.Reflections;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public class DelegatingClassLoader extends URLClassLoader {
private static final Logger log = LoggerFactory.getLogger(DelegatingClassLoader.class);
private final Map<String, SortedMap<PluginDesc<?>, ClassLoader>> pluginLoaders;
private final SortedSet<PluginDesc<Connector>> connectors;
private final SortedSet<PluginDesc<Converter>> converters;
private final SortedSet<PluginDesc<Transformation>> transformations;
private final List<String> pluginPaths;
private final Map<Path, PluginClassLoader> activePaths;
public DelegatingClassLoader(List<String> pluginPaths, ClassLoader parent) {
super(new URL[0], parent);
this.pluginPaths = pluginPaths;
this.pluginLoaders = new HashMap<>();
this.activePaths = new HashMap<>();
this.connectors = new TreeSet<>();
this.converters = new TreeSet<>();
this.transformations = new TreeSet<>();
}
public DelegatingClassLoader(List<String> pluginPaths) {
this(pluginPaths, ClassLoader.getSystemClassLoader());
}
public Set<PluginDesc<Connector>> connectors() {
return connectors;
}
public Set<PluginDesc<Converter>> converters() {
return converters;
}
public Set<PluginDesc<Transformation>> transformations() {
return transformations;
}
public ClassLoader connectorLoader(Connector connector) {
return connectorLoader(connector.getClass().getName());
}
public ClassLoader connectorLoader(String connectorClassOrAlias) {
log.debug("Getting plugin class loader for connector: '{}'", connectorClassOrAlias);
SortedMap<PluginDesc<?>, ClassLoader> inner =
pluginLoaders.get(connectorClassOrAlias);
if (inner == null) {
log.error(
"Plugin class loader for connector: '{}' was not found. Returning: {}",
connectorClassOrAlias,
this
);
return this;
}
return inner.get(inner.lastKey());
}
private static PluginClassLoader newPluginClassLoader(
final URL pluginLocation,
final URL[] urls,
final ClassLoader parent
) {
return (PluginClassLoader) AccessController.doPrivileged(
new PrivilegedAction() {
@Override
public Object run() {
return new PluginClassLoader(pluginLocation, urls, parent);
}
}
);
}
private <T> void addPlugins(Collection<PluginDesc<T>> plugins, ClassLoader loader) {
for (PluginDesc<T> plugin : plugins) {
String pluginClassName = plugin.className();
SortedMap<PluginDesc<?>, ClassLoader> inner = pluginLoaders.get(pluginClassName);
if (inner == null) {
inner = new TreeMap<>();
pluginLoaders.put(pluginClassName, inner);
// TODO: once versioning is enabled this line should be moved outside this if branch
log.info("Added plugin '{}'", pluginClassName);
}
inner.put(plugin, loader);
}
}
protected void initLoaders() {
String path = null;
try {
for (String configPath : pluginPaths) {
path = configPath;
Path pluginPath = Paths.get(path).toAbsolutePath();
// Currently 'plugin.paths' property is a list of top-level directories
// containing plugins
if (Files.isDirectory(pluginPath)) {
for (Path pluginLocation : PluginUtils.pluginLocations(pluginPath)) {
log.info("Loading plugin from: {}", pluginLocation);
URL[] urls = PluginUtils.pluginUrls(pluginLocation).toArray(new URL[0]);
if (log.isDebugEnabled()) {
log.debug("Loading plugin urls: {}", Arrays.toString(urls));
}
PluginClassLoader loader = newPluginClassLoader(
pluginLocation.toUri().toURL(),
urls,
this
);
scanUrlsAndAddPlugins(loader, urls, pluginLocation);
}
}
}
path = "classpath";
// Finally add parent/system loader.
scanUrlsAndAddPlugins(
getParent(),
ClasspathHelper.forJavaClassPath().toArray(new URL[0]),
null
);
} catch (InvalidPathException | MalformedURLException e) {
log.error("Invalid path in plugin path: {}. Ignoring.", path);
} catch (IOException e) {
log.error("Could not get listing for plugin path: {}. Ignoring.", path);
} catch (InstantiationException | IllegalAccessException e) {
log.error("Could not instantiate plugins in: {}. Ignoring: {}", path, e);
}
addAllAliases();
}
private void scanUrlsAndAddPlugins(
ClassLoader loader,
URL[] urls,
Path pluginLocation
) throws InstantiationException, IllegalAccessException {
PluginScanResult plugins = scanPluginPath(loader, urls);
log.info("Registered loader: {}", loader);
if (!plugins.isEmpty()) {
if (loader instanceof PluginClassLoader) {
activePaths.put(pluginLocation, (PluginClassLoader) loader);
}
addPlugins(plugins.connectors(), loader);
connectors.addAll(plugins.connectors());
addPlugins(plugins.converters(), loader);
converters.addAll(plugins.converters());
addPlugins(plugins.transformations(), loader);
transformations.addAll(plugins.transformations());
}
}
private PluginScanResult scanPluginPath(
ClassLoader loader,
URL[] urls
) throws InstantiationException, IllegalAccessException {
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.setClassLoaders(new ClassLoader[]{loader});
builder.addUrls(urls);
Reflections reflections = new Reflections(builder);
return new PluginScanResult(
getPluginDesc(reflections, Connector.class, loader),
getPluginDesc(reflections, Converter.class, loader),
getPluginDesc(reflections, Transformation.class, loader)
);
}
private <T> Collection<PluginDesc<T>> getPluginDesc(
Reflections reflections,
Class<T> klass,
ClassLoader loader
) throws InstantiationException, IllegalAccessException {
Set<Class<? extends T>> plugins = reflections.getSubTypesOf(klass);
Collection<PluginDesc<T>> result = new ArrayList<>();
for (Class<? extends T> plugin : plugins) {
if (PluginUtils.isConcrete(plugin)) {
// Temporary workaround until all the plugins are versioned.
if (Connector.class.isAssignableFrom(plugin)) {
result.add(
new PluginDesc<>(
plugin,
((Connector) plugin.newInstance()).version(),
loader
)
);
} else {
result.add(new PluginDesc<>(plugin, "undefined", loader));
}
}
}
return result;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (!PluginUtils.shouldLoadInIsolation(name)) {
// There are no paths in this classloader, will attempt to load with the parent.
return super.loadClass(name, resolve);
}
SortedMap<PluginDesc<?>, ClassLoader> inner = pluginLoaders.get(name);
if (inner != null) {
log.trace("Retrieving loaded class '{}' from '{}'", name, inner.get(inner.lastKey()));
ClassLoader pluginLoader = inner.get(inner.lastKey());
return pluginLoader instanceof PluginClassLoader
? ((PluginClassLoader) pluginLoader).loadClass(name, resolve)
: super.loadClass(name, resolve);
}
Class<?> klass = null;
for (PluginClassLoader loader : activePaths.values()) {
try {
klass = loader.loadClass(name, resolve);
break;
} catch (ClassNotFoundException e) {
// Not found in this loader.
}
}
if (klass == null) {
return super.loadClass(name, resolve);
}
return klass;
}
private void addAllAliases() {
addAliases(connectors);
addAliases(converters);
addAliases(transformations);
}
private <S> void addAliases(Collection<PluginDesc<S>> plugins) {
for (PluginDesc<S> plugin : plugins) {
if (PluginUtils.isAliasUnique(plugin, plugins)) {
String simple = PluginUtils.simpleName(plugin);
String pruned = PluginUtils.prunedName(plugin);
SortedMap<PluginDesc<?>, ClassLoader> inner = pluginLoaders.get(plugin.className());
pluginLoaders.put(simple, inner);
if (simple.equals(pruned)) {
log.info("Added alias '{}' to plugin '{}'", simple, plugin.className());
} else {
pluginLoaders.put(pruned, inner);
log.info(
"Added aliases '{}' and '{}' to plugin '{}'",
simple,
pruned,
plugin.className()
);
}
}
}
}
}