/*
* Copyright © 2015 Cask Data, Inc.
*
* Licensed 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 co.cask.cdap.internal.app.runtime.plugin;
import co.cask.cdap.api.annotation.Name;
import co.cask.cdap.api.artifact.ArtifactId;
import co.cask.cdap.api.data.schema.UnsupportedTypeException;
import co.cask.cdap.api.plugin.Plugin;
import co.cask.cdap.api.plugin.PluginClass;
import co.cask.cdap.api.plugin.PluginConfig;
import co.cask.cdap.api.plugin.PluginProperties;
import co.cask.cdap.api.plugin.PluginPropertyField;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.lang.InstantiatorFactory;
import co.cask.cdap.common.lang.jar.BundleJarUtil;
import co.cask.cdap.common.utils.DirUtils;
import co.cask.cdap.internal.app.runtime.artifact.Artifacts;
import co.cask.cdap.internal.lang.FieldVisitor;
import co.cask.cdap.internal.lang.Fields;
import co.cask.cdap.internal.lang.Reflections;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeToken;
import org.apache.twill.filesystem.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.concurrent.ExecutionException;
/**
* This class helps creating new instances of plugins. It also contains a ClassLoader cache to
* save ClassLoader creation.
*
* This class implements {@link Closeable} as well for cleanup of temporary directories created for the ClassLoaders.
*/
public class PluginInstantiator implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(PluginInstantiator.class);
private final LoadingCache<ArtifactId, ClassLoader> classLoaders;
private final InstantiatorFactory instantiatorFactory;
private final File tmpDir;
private final File pluginDir;
private final ClassLoader parentClassLoader;
public PluginInstantiator(CConfiguration cConf, ClassLoader parentClassLoader, File pluginDir) {
this.instantiatorFactory = new InstantiatorFactory(false);
File tmpDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR),
cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
this.pluginDir = pluginDir;
this.tmpDir = DirUtils.createTempDir(tmpDir);
this.classLoaders = CacheBuilder.newBuilder()
.removalListener(new ClassLoaderRemovalListener())
.build(new ClassLoaderCacheLoader());
this.parentClassLoader = PluginClassLoader.createParent(parentClassLoader);
}
/**
* Adds a artifact Jar present at the given {@link Location} to allow Plugin Instantiator to load the class
*
* @param artifactLocation Location of the Artifact JAR
* @param destArtifact {@link ArtifactId} of the plugin
* @throws IOException if failed to copy the artifact JAR
*/
public void addArtifact(Location artifactLocation, ArtifactId destArtifact) throws IOException {
File destFile = new File(pluginDir, Artifacts.getFileName(destArtifact));
Files.copy(Locations.newInputSupplier(artifactLocation), destFile);
}
/**
* Returns a {@link ClassLoader} for the given artifact.
*
* @param artifactId {@link ArtifactId}
* @throws IOException if failed to expand the artifact jar to create the plugin ClassLoader
*
* @see PluginClassLoader
*/
public ClassLoader getArtifactClassLoader(ArtifactId artifactId) throws IOException {
try {
return classLoaders.get(artifactId);
} catch (ExecutionException e) {
Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
throw Throwables.propagate(e.getCause());
}
}
/**
* Loads and returns the {@link Class} of the given plugin.
*
* @param plugin {@link Plugin}
* @param <T> Type of the plugin
* @return the plugin Class
* @throws IOException if failed to expand the plugin jar to create the plugin ClassLoader
* @throws ClassNotFoundException if failed to load the given plugin class
*/
@SuppressWarnings("unchecked")
public <T> Class<T> loadClass(Plugin plugin) throws IOException, ClassNotFoundException {
return (Class<T>) getArtifactClassLoader(plugin.getArtifactId()).loadClass(plugin.getPluginClass().getClassName());
}
/**
* Create a new instance of plugin class without any config, config will be null in the instantiated plugin.
* @param artifact artifact of the plugin
* @param pluginClass information about plugin class
* @param <T> Type of plugin
* @return a new plugin instance
*/
public <T> T newInstanceWithoutConfig(ArtifactId artifact,
PluginClass pluginClass) throws IOException, ClassNotFoundException {
ClassLoader pluginClassLoader = getArtifactClassLoader(artifact);
Class pluginClassLoaded = pluginClassLoader.loadClass(pluginClass.getClassName());
return (T) instantiatorFactory.get(TypeToken.of(pluginClassLoaded)).create();
}
/**
* Creates a new instance of the given plugin class.
*
* @param plugin {@link Plugin}
* @param <T> Type of the plugin
* @return a new plugin instance
* @throws IOException if failed to expand the plugin jar to create the plugin ClassLoader
* @throws ClassNotFoundException if failed to load the given plugin class
*/
@SuppressWarnings("unchecked")
public <T> T newInstance(Plugin plugin) throws IOException, ClassNotFoundException {
ClassLoader classLoader = getArtifactClassLoader(plugin.getArtifactId());
PluginClass pluginClass = plugin.getPluginClass();
TypeToken<?> pluginType = TypeToken.of(classLoader.loadClass(pluginClass.getClassName()));
try {
String configFieldName = pluginClass.getConfigFieldName();
// Plugin doesn't have config. Simply return a new instance.
if (configFieldName == null) {
return (T) instantiatorFactory.get(pluginType).create();
}
// Create the config instance
Field field = Fields.findField(pluginType.getType(), configFieldName);
TypeToken<?> configFieldType = pluginType.resolveType(field.getGenericType());
Object config = instantiatorFactory.get(configFieldType).create();
Reflections.visit(config, configFieldType.getType(),
new ConfigFieldSetter(pluginClass, plugin.getArtifactId(), plugin.getProperties()));
// Create the plugin instance
return newInstance(pluginType, field, configFieldType, config);
} catch (NoSuchFieldException e) {
throw new InvalidPluginConfigException("Config field not found in plugin class: " + pluginClass, e);
} catch (IllegalAccessException e) {
throw new InvalidPluginConfigException("Failed to set plugin config field: " + pluginClass, e);
}
}
/**
* Creates a new plugin instance and optionally setup the {@link PluginConfig} field.
*/
@SuppressWarnings("unchecked")
private <T> T newInstance(TypeToken<?> pluginType, Field configField,
TypeToken<?> configFieldType, Object config) throws IllegalAccessException {
// See if the plugin has a constructor that takes the config type.
// Need to loop because we need to resolve the constructor parameter type from generic.
for (Constructor<?> constructor : pluginType.getRawType().getConstructors()) {
Type[] parameterTypes = constructor.getGenericParameterTypes();
if (parameterTypes.length != 1) {
continue;
}
if (configFieldType.equals(pluginType.resolveType(parameterTypes[0]))) {
constructor.setAccessible(true);
try {
// Call the plugin constructor to construct the instance
return (T) constructor.newInstance(config);
} catch (Exception e) {
// Failed to instantiate. Resort to field injection
LOG.warn("Failed to invoke plugin constructor {}. Resort to config field injection.", constructor);
break;
}
}
}
// No matching constructor found, do field injection.
T plugin = (T) instantiatorFactory.get(pluginType).create();
configField.setAccessible(true);
configField.set(plugin, config);
return plugin;
}
@Override
public void close() throws IOException {
// Cleanup the ClassLoader cache and the temporary directory for the expanded plugin jar.
classLoaders.invalidateAll();
if (parentClassLoader instanceof Closeable) {
Closeables.closeQuietly((Closeable) parentClassLoader);
}
try {
DirUtils.deleteDirectoryContents(tmpDir);
} catch (IOException e) {
// It's the cleanup step. Nothing much can be done if cleanup failed.
LOG.warn("Failed to delete directory {}", tmpDir);
}
}
/**
* A CacheLoader for creating plugin ClassLoader.
*/
private final class ClassLoaderCacheLoader extends CacheLoader<ArtifactId, ClassLoader> {
@Override
public ClassLoader load(ArtifactId artifactId) throws Exception {
File unpackedDir = DirUtils.createTempDir(tmpDir);
File artifact = new File(pluginDir, Artifacts.getFileName(artifactId));
BundleJarUtil.unJar(Locations.toLocation(artifact), unpackedDir);
return new PluginClassLoader(unpackedDir, parentClassLoader);
}
}
/**
* A RemovalListener for closing plugin ClassLoader.
*/
private static final class ClassLoaderRemovalListener implements RemovalListener<ArtifactId, ClassLoader> {
@Override
public void onRemoval(RemovalNotification<ArtifactId, ClassLoader> notification) {
ClassLoader cl = notification.getValue();
if (cl instanceof Closeable) {
Closeables.closeQuietly((Closeable) cl);
}
}
}
/**
* A {@link FieldVisitor} for setting values into {@link PluginConfig} object based on {@link PluginProperties}.
*/
private static final class ConfigFieldSetter extends FieldVisitor {
private final PluginClass pluginClass;
private final PluginProperties properties;
private final ArtifactId artifactId;
public ConfigFieldSetter(PluginClass pluginClass, ArtifactId artifactId,
PluginProperties properties) {
this.pluginClass = pluginClass;
this.artifactId = artifactId;
this.properties = properties;
}
@Override
public void visit(Object instance, Type inspectType, Type declareType, Field field) throws Exception {
int modifiers = field.getModifiers();
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers) || field.isSynthetic()) {
return;
}
TypeToken<?> declareTypeToken = TypeToken.of(declareType);
if (PluginConfig.class.equals(declareTypeToken.getRawType())) {
if (field.getName().equals("properties")) {
field.set(instance, properties);
}
return;
}
Name nameAnnotation = field.getAnnotation(Name.class);
String name = nameAnnotation == null ? field.getName() : nameAnnotation.value();
PluginPropertyField pluginPropertyField = pluginClass.getProperties().get(name);
if (pluginPropertyField.isRequired() && !properties.getProperties().containsKey(name)) {
throw new IllegalArgumentException("Missing required plugin property " + name
+ " for " + pluginClass.getName() + " in artifact " + artifactId);
}
String value = properties.getProperties().get(name);
if (pluginPropertyField.isRequired() || value != null) {
field.set(instance, convertValue(declareTypeToken.resolveType(field.getGenericType()), value));
}
}
/**
* Converts string value into value of the fieldType.
*/
private Object convertValue(TypeToken<?> fieldType, String value) throws Exception {
// Currently we only support primitive, wrapped primitive and String types.
Class<?> rawType = fieldType.getRawType();
if (String.class.equals(rawType)) {
return value;
}
if (rawType.isPrimitive()) {
rawType = Primitives.wrap(rawType);
}
if (Primitives.isWrapperType(rawType)) {
Method valueOf = rawType.getMethod("valueOf", String.class);
try {
return valueOf.invoke(null, value);
} catch (InvocationTargetException e) {
if (e.getCause() instanceof NumberFormatException) {
// if exception is due to wrong value for integer/double conversion
throw new InvalidPluginConfigException(String.format("valueOf operation on %s failed", value),
e.getCause());
}
throw e;
}
}
throw new UnsupportedTypeException("Only primitive and String types are supported");
}
}
}