Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.cdap.cdap.internal.app.runtime.plugin.PluginInstantiator Maven / Gradle / Ivy
/*
* Copyright © 2015-2021 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 io.cdap.cdap.internal.app.runtime.plugin;
import com.google.common.base.Defaults;
import com.google.common.base.Strings;
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.collect.ImmutableMap;
import com.google.common.io.Closeables;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import io.cdap.cdap.api.annotation.Name;
import io.cdap.cdap.api.artifact.ArtifactId;
import io.cdap.cdap.api.macro.InvalidMacroException;
import io.cdap.cdap.api.macro.MacroEvaluator;
import io.cdap.cdap.api.macro.MacroParserOptions;
import io.cdap.cdap.api.plugin.InvalidPluginConfigException;
import io.cdap.cdap.api.plugin.InvalidPluginProperty;
import io.cdap.cdap.api.plugin.Plugin;
import io.cdap.cdap.api.plugin.PluginClass;
import io.cdap.cdap.api.plugin.PluginConfig;
import io.cdap.cdap.api.plugin.PluginProperties;
import io.cdap.cdap.api.plugin.PluginPropertyField;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.io.Locations;
import io.cdap.cdap.common.lang.CombineClassLoader;
import io.cdap.cdap.common.lang.InstantiatorFactory;
import io.cdap.cdap.common.lang.jar.BundleJarUtil;
import io.cdap.cdap.common.lang.jar.ClassLoaderFolder;
import io.cdap.cdap.common.utils.DirUtils;
import io.cdap.cdap.internal.app.runtime.artifact.Artifacts;
import io.cdap.cdap.internal.lang.FieldVisitor;
import io.cdap.cdap.internal.lang.Fields;
import io.cdap.cdap.internal.lang.Reflections;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.twill.filesystem.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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);
// used for setting defaults of string and non-string macro-enabled properties at config time
private static final Map> PROPERTY_TYPES = ImmutableMap.>builder()
.put("boolean", boolean.class)
.put("byte", byte.class)
.put("char", char.class)
.put("double", double.class)
.put("int", int.class)
.put("float", float.class)
.put("long", long.class)
.put("short", short.class)
.put("string", String.class)
.build();
private static final Type MAP_STRING_TYPE = new TypeToken>() {
}.getType();
private final LoadingCache classLoaders;
private final InstantiatorFactory instantiatorFactory;
private final File tmpDir;
private final File pluginDir;
private final ClassLoader parentClassLoader;
private final boolean ownedParentClassLoader;
private final Gson gson;
public PluginInstantiator(CConfiguration cConf, ClassLoader parentClassLoader, File pluginDir) {
this(cConf, parentClassLoader, pluginDir, true);
}
public PluginInstantiator(CConfiguration cConf, ClassLoader parentClassLoader, File pluginDir,
boolean filterClassloader) {
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 =
filterClassloader ? PluginClassLoader.createParent(parentClassLoader) : parentClassLoader;
this.ownedParentClassLoader = filterClassloader;
// Don't use a static Gson object to avoid caching of classloader, which can cause classloader leakage.
this.gson = new GsonBuilder().setFieldNamingStrategy(new PluginFieldNamingStrategy()).create();
}
/**
* 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));
if (!destFile.exists()) {
if ("file".equals(artifactLocation.toURI().getScheme()) && artifactLocation.isDirectory()) {
Files.createSymbolicLink(destFile.toPath(), Paths.get(artifactLocation.toURI()));
} else {
Locations.linkOrCopy(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 PluginClassLoader getArtifactClassLoader(ArtifactId artifactId) throws IOException {
try {
return classLoaders.get(new ClassLoaderKey(artifactId));
} catch (ExecutionException e) {
Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
throw Throwables.propagate(e.getCause());
}
}
/**
* Returns a {@link ClassLoader} for the given plugin.
*
* @param plugin {@link Plugin}
* @throws IOException if failed to expand the artifact jar to create the plugin ClassLoader
* @see PluginClassLoader
*/
public ClassLoader getPluginClassLoader(Plugin plugin) throws IOException {
return getPluginClassLoader(plugin.getArtifactId(), plugin.getParents());
}
/**
* Returns a {@link ClassLoader} for the given plugin.
*
* @param artifactId the artifact id of the plugin
* @param pluginParents the list of parents' artifact id of the plugin that are also plugins
* @throws IOException if failed to expand the artifact jar to create the plugin ClassLoader
* @see PluginClassLoader
*/
public PluginClassLoader getPluginClassLoader(ArtifactId artifactId,
List pluginParents) throws IOException {
try {
return classLoaders.get(new ClassLoaderKey(artifactId, pluginParents));
} 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 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 Class loadClass(Plugin plugin) throws IOException, ClassNotFoundException {
return (Class) getPluginClassLoader(plugin).loadClass(
plugin.getPluginClass().getClassName());
}
/**
* Creates a new instance of the given plugin class.
*
* @param plugin {@link Plugin}
* @param Type of the plugin
* @return a new plugin instance with macros substituted
* @throws IOException if failed to expand the plugin jar to create the plugin ClassLoader
* @throws ClassNotFoundException if failed to load the given plugin class
* @throws InvalidPluginConfigException if the PluginConfig could not be created from the
* plugin properties
*/
public T newInstance(Plugin plugin)
throws IOException, ClassNotFoundException, InvalidMacroException {
return newInstance(plugin, null);
}
/**
* Creates a new instance of the given plugin class with all property macros substituted if a
* MacroEvaluator is given. At runtime, plugin property fields that are macro-enabled and contain
* macro syntax will remain in the macroFields set in the plugin config.
*
* @param plugin {@link Plugin}
* @param macroEvaluator the MacroEvaluator that performs macro substitution
* @param Type of the plugin
* @return a new plugin instance with macros substituted
* @throws IOException if failed to expand the plugin jar to create the plugin ClassLoader
* @throws ClassNotFoundException if failed to load the given plugin class
* @throws InvalidPluginConfigException if the PluginConfig could not be created from the
* plugin properties
*/
public T newInstance(Plugin plugin, @Nullable MacroEvaluator macroEvaluator)
throws IOException, ClassNotFoundException, InvalidMacroException {
return newInstance(plugin, macroEvaluator, null);
}
/**
* Creates a new instance of the given plugin class with all property macros substituted if a
* MacroEvaluator is given. At runtime, plugin property fields that are macro-enabled and contain
* macro syntax will remain in the macroFields set in the plugin config.
*
* @param plugin {@link Plugin}
* @param macroEvaluator the MacroEvaluator that performs macro substitution
* @param options macro parser options
* @param Type of the plugin
* @return a new plugin instance with macros substituted
* @throws IOException if failed to expand the plugin jar to create the plugin ClassLoader
* @throws ClassNotFoundException if failed to load the given plugin class
* @throws InvalidPluginConfigException if the PluginConfig could not be created from the
* plugin properties
*/
public T newInstance(
Plugin plugin, @Nullable MacroEvaluator macroEvaluator,
@Nullable MacroParserOptions options)
throws IOException, ClassNotFoundException, InvalidMacroException {
ClassLoader classLoader = getPluginClassLoader(plugin);
PluginClass pluginClass = plugin.getPluginClass();
@SuppressWarnings("unchecked")
TypeToken pluginType = TypeToken.of(
(Class) classLoader.loadClass(pluginClass.getClassName()));
try {
String configFieldName = pluginClass.getConfigFieldName();
// Plugin doesn't have config. Simply return a new instance.
if (configFieldName == null) {
return 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();
// perform macro substitution if an evaluator is provided, collect fields with macros only at configure time
PluginProperties pluginProperties = substituteMacros(plugin, macroEvaluator, options);
Set macroFields =
(macroEvaluator == null) ? getFieldsWithMacro(plugin) : Collections.emptySet();
PluginProperties rawProperties = plugin.getProperties();
ConfigFieldSetter fieldSetter = new ConfigFieldSetter(pluginClass, pluginProperties,
rawProperties, macroFields);
Reflections.visit(config, configFieldType.getType(), fieldSetter);
if (!fieldSetter.invalidProperties.isEmpty() || !fieldSetter.missingProperties.isEmpty()) {
throw new InvalidPluginConfigException(pluginClass, fieldSetter.missingProperties,
fieldSetter.invalidProperties);
}
// 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);
}
}
public PluginProperties substituteMacros(Plugin plugin, @Nullable MacroEvaluator macroEvaluator,
@Nullable MacroParserOptions options) {
Map properties = new HashMap<>();
Map pluginPropertyFieldMap = plugin.getPluginClass()
.getProperties();
// create macro evaluator and parser based on if it is config or runtime
boolean configTime = (macroEvaluator == null);
TrackingMacroEvaluator trackingMacroEvaluator = new TrackingMacroEvaluator();
for (Map.Entry property : plugin.getProperties().getProperties().entrySet()) {
PluginPropertyField field = pluginPropertyFieldMap.get(property.getKey());
String propertyValue = property.getValue();
if (field != null && field.isMacroSupported()) {
// TODO: cleanup after endpoint to get plugin details is merged (#6089)
if (configTime) {
// parse for syntax check and check if trackingMacroEvaluator finds macro syntax present
MacroParser macroParser = new MacroParser(trackingMacroEvaluator,
MacroParserOptions.builder()
.setEscaping(field.isMacroEscapingEnabled())
.build());
macroParser.parse(propertyValue);
// if the field is a nested field and it has macro in it, there are two scenarios:
// 1. the field itself needs to get evaluated, for example, ${conn(test)}
// 2. the field itself is already a map json string, but some field inside the map is a macro, for example,
// secure macros.
if (!field.getChildren().isEmpty() && trackingMacroEvaluator.hasMacro()) {
try {
Map childMap = gson.fromJson(propertyValue, MAP_STRING_TYPE);
// if this is already a map, this is scenario 2, we need to get the default value for the field
// one by one depending on if there is a macro in it
trackingMacroEvaluator.reset();
Map substitutedChildMap = new HashMap<>();
childMap.forEach((name, value) -> {
if (!pluginPropertyFieldMap.containsKey(name)) {
return;
}
macroParser.parse(value);
substitutedChildMap.put(name, getOriginalOrDefaultValue(
value, name, pluginPropertyFieldMap.get(name).getType(),
trackingMacroEvaluator));
});
propertyValue = gson.toJson(substitutedChildMap);
} catch (JsonSyntaxException e) {
// this is scenario 1, just continue
}
}
propertyValue = getOriginalOrDefaultValue(propertyValue, property.getKey(),
field.getType(),
trackingMacroEvaluator);
} else {
MacroParserOptions parserOptions = options == null ? MacroParserOptions.builder()
.setEscaping(field.isMacroEscapingEnabled())
.build() : options;
MacroParser macroParser = new MacroParser(macroEvaluator, parserOptions);
String oldValue = propertyValue;
propertyValue = macroParser.parse(propertyValue);
// There is a special case when the field can contain nested fields:
// At runtime, the value of this field should be a json map, with possibly unevaluated macro inside,
// i.e, secure macro is not evaluated when regenerating app spec.
// Therefore, if the value of this macro is a also a json, the combined string itself is no longer
// a valid json map, since the replaced value is not escaped.
// So we do the following check to verify it is a valid json map, if not,
// we convert old value to the map first, and evaluate the map fields one by one, then convert it
// back to ensure it is a valid json map
if (!field.getChildren().isEmpty() && propertyValue != null
&& !parserOptions.shouldSkipInvalid()) {
try {
gson.fromJson(propertyValue, MAP_STRING_TYPE);
} catch (JsonSyntaxException e) {
// convert using old value
Map unevaluatedProperties = gson.fromJson(oldValue, MAP_STRING_TYPE);
Map evaluated = new HashMap<>();
unevaluatedProperties.forEach(
(key, val) -> evaluated.put(key, macroParser.parse(val)));
propertyValue = gson.toJson(evaluated);
}
}
}
}
properties.put(property.getKey(), propertyValue);
}
return PluginProperties.builder().addAll(properties).build();
}
private String getOriginalOrDefaultValue(String originalPropertyString, String propertyName,
String propertyType,
TrackingMacroEvaluator trackingMacroEvaluator) {
if (trackingMacroEvaluator.hasMacro()) {
trackingMacroEvaluator.reset();
return getDefaultProperty(propertyType);
}
return originalPropertyString;
}
private String getDefaultProperty(String propertyType) {
Class propertyClass = PROPERTY_TYPES.get(propertyType);
if (propertyClass == null) {
return null;
}
Object defaultProperty = Defaults.defaultValue(propertyClass);
return defaultProperty == null ? null : defaultProperty.toString();
}
private Set getFieldsWithMacro(Plugin plugin) {
// TODO: cleanup after endpoint to get plugin details is merged (#6089)
Set macroFields = new HashSet<>();
Map pluginPropertyFieldMap = plugin.getPluginClass()
.getProperties();
TrackingMacroEvaluator trackingMacroEvaluator = new TrackingMacroEvaluator();
for (Map.Entry pluginEntry : pluginPropertyFieldMap.entrySet()) {
PluginPropertyField pluginField = pluginEntry.getValue();
if (pluginEntry.getValue() != null && pluginField.isMacroSupported()) {
String macroValue = plugin.getProperties().getProperties().get(pluginEntry.getKey());
if (macroValue != null) {
MacroParser macroParser = new MacroParser(trackingMacroEvaluator,
MacroParserOptions.builder()
.setEscaping(pluginField.isMacroEscapingEnabled())
.build());
macroParser.parse(macroValue);
if (trackingMacroEvaluator.hasMacro()) {
macroFields.add(pluginEntry.getKey());
trackingMacroEvaluator.reset();
if (pluginField.getChildren().isEmpty()) {
continue;
}
// if the field is a nested field and it has macro in it, there are two scenarios:
// 1. the field itself needs to get evaluated, for example, ${conn(test)}
// 2. the field itself is already a map json string, but some field inside the map is a macro, for example,
// secure macros.
try {
Map childMap = gson.fromJson(macroValue, MAP_STRING_TYPE);
// if this is already a map, this is scenario 2, we need to check the fields one by one
macroFields.remove(pluginEntry.getKey());
childMap.forEach((name, value) -> {
if (value != null) {
macroParser.parse(value);
if (trackingMacroEvaluator.hasMacro()) {
macroFields.add(name);
}
trackingMacroEvaluator.reset();
}
});
} catch (JsonSyntaxException e) {
// this is scenario 1, then mark all the fields inside the field as macro
macroFields.addAll(pluginField.getChildren());
}
}
}
}
}
return macroFields;
}
/**
* Creates a new plugin instance and optionally setup the {@link PluginConfig} field.
*/
@SuppressWarnings("unchecked")
private 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 (InvocationTargetException e) {
// If there is exception thrown from the constructor, propagate it.
throw Throwables.propagate(e.getCause());
} 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 (ownedParentClassLoader) {
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);
}
}
/**
* Key for the classloader cache.
*/
private static class ClassLoaderKey {
private final List parents;
private final ArtifactId artifact;
ClassLoaderKey(ArtifactId artifact) {
this(artifact, Collections.emptyList());
}
ClassLoaderKey(ArtifactId artifact, List parents) {
this.parents = parents;
this.artifact = artifact;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ClassLoaderKey that = (ClassLoaderKey) o;
return Objects.equals(parents, that.parents) && Objects.equals(artifact, that.artifact);
}
@Override
public int hashCode() {
return Objects.hash(parents, artifact);
}
}
/**
* A CacheLoader for creating plugin ClassLoader.
*/
private final class ClassLoaderCacheLoader extends
CacheLoader {
@Override
public PluginClassLoader load(ClassLoaderKey key) throws Exception {
File artifact = new File(pluginDir, Artifacts.getFileName(key.artifact));
ClassLoaderFolder classLoaderFolder = BundleJarUtil.prepareClassLoaderFolder(
Locations.toLocation(artifact), () -> DirUtils.createTempDir(tmpDir));
Iterator parentIter = key.parents.iterator();
if (!parentIter.hasNext()) {
return new PluginClassLoader(key.artifact, classLoaderFolder.getDir(),
artifact.getAbsolutePath(), parentClassLoader);
}
List parentsOfParent = new ArrayList<>(key.parents.size() - 1);
ArtifactId parentArtifact = parentIter.next();
while (parentIter.hasNext()) {
parentsOfParent.add(parentIter.next());
}
/*
* Combine CL [filtered grandparentCL (export-packages only),
* filtered parentPluginCL (export-packages only)]
* ^
* |
* |
* Plugin CL (classes in plugin artifact)
*
* The plugin classloader's parent will have whatever is exported by the parent plugin, and whatever
* is exported by the grandparent. Today, since we don't allow past a grandparent, the grandparent should
* always be the filtered program classloader. But if we change it to allow arbitrary levels, the grandparent
* could be another plugin. In effect, the plugin should have access to everything exported by plugins and apps
* above it.
*/
PluginClassLoader parentPluginCL = getPluginClassLoader(parentArtifact, parentsOfParent);
ClassLoader parentCL =
new CombineClassLoader(parentPluginCL.getParent(),
parentPluginCL.getExportPackagesClassLoader());
return new PluginClassLoader(key.artifact, classLoaderFolder.getDir(),
artifact.getAbsolutePath(), parentCL);
}
}
/**
* A RemovalListener for closing plugin ClassLoader.
*/
private static final class ClassLoaderRemovalListener implements
RemovalListener {
@Override
public void onRemoval(RemovalNotification notification) {
Closeables.closeQuietly(notification.getValue());
}
}
/**
* 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 PluginProperties rawProperties;
private final Set macroFields;
private final Set missingProperties;
private final Set invalidProperties;
private final Gson gson;
ConfigFieldSetter(PluginClass pluginClass, PluginProperties properties,
PluginProperties rawProperties,
Set macroFields) {
this.pluginClass = pluginClass;
this.properties = properties;
this.rawProperties = rawProperties;
this.macroFields = macroFields;
this.missingProperties = new HashSet<>();
this.invalidProperties = new HashSet<>();
// Don't use a static Gson object to avoid caching of classloader, which can cause classloader leakage.
this.gson = new GsonBuilder().setFieldNamingStrategy(new PluginFieldNamingStrategy())
.create();
}
@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())) {
switch (field.getName()) {
case "properties":
field.set(instance, properties);
break;
case "macroFields":
field.set(instance, macroFields);
break;
case "rawProperties":
field.set(instance, rawProperties);
break;
}
return;
}
Name nameAnnotation = field.getAnnotation(Name.class);
String name = nameAnnotation == null ? field.getName() : nameAnnotation.value();
PluginPropertyField pluginPropertyField = pluginClass.getProperties().get(name);
// if the property is required and it's not a macro and the property doesn't exist and it is not an config
// that is consisted of a collection of configs
Set children = pluginPropertyField.getChildren();
if (pluginPropertyField.isRequired()
&& !macroFields.contains(name)
&& properties.getProperties().get(name) == null
&& children.isEmpty()) {
missingProperties.add(name);
return;
}
String value = properties.getProperties().get(name);
// if the value is null but this field is consisted of children, look up all the child properties to build
// the config
if (value == null && !children.isEmpty()) {
Map childProperties = new HashMap<>();
boolean missing = false;
for (String child : children) {
PluginPropertyField childProperty = pluginClass.getProperties().get(child);
// if child property is required and it is missing, add it to missing properties and continue
if (childProperty.isRequired() && !macroFields.contains(child)
&& !properties.getProperties().containsKey(child)) {
missingProperties.add(child);
missing = true;
continue;
}
childProperties.put(child, properties.getProperties().get(child));
}
// if missing any required field, return here
if (missing) {
return;
}
value = gson.toJson(childProperties);
}
if (pluginPropertyField.isRequired() || value != null) {
try {
Object convertedValue = convertValue(name, declareType,
declareTypeToken.resolveType(field.getGenericType()), value);
// set the remaining plugin properties field
if (!children.isEmpty() && convertedValue instanceof PluginConfig) {
PluginConfig config = (PluginConfig) convertedValue;
setChildPluginConfigField(config, "properties", PluginProperties.builder().addAll(
properties.getProperties().entrySet().stream()
.filter(entry -> children.contains(entry.getKey()))
// Collectors.toMap does not take null entry value, so use HashMap instead
.collect(HashMap::new, (map, entry) -> map.put(entry.getKey(),
entry.getValue()), HashMap::putAll)).build());
setChildPluginConfigField(config, "rawProperties", PluginProperties.builder().addAll(
rawProperties.getProperties().entrySet().stream()
.filter(entry -> children.contains(entry.getKey()))
.collect(HashMap::new, (map, entry) -> map.put(entry.getKey(),
entry.getValue()), HashMap::putAll)).build());
setChildPluginConfigField(config, "macroFields",
macroFields.stream().filter(children::contains).collect(Collectors.toSet()));
}
field.set(instance, convertedValue);
} catch (Exception e) {
invalidProperties.add(new InvalidPluginProperty(name, e));
}
}
}
private void setChildPluginConfigField(PluginConfig config, String fieldName,
Object fieldVal) throws NoSuchFieldException, IllegalAccessException {
Field childField = PluginConfig.class.getDeclaredField(fieldName);
childField.setAccessible(true);
childField.set(config, fieldVal);
}
/**
* Converts string value into value of the fieldType.
*/
private Object convertValue(String name, Type declareType, TypeToken fieldType, String value)
throws Exception {
// For primitive, wrapped primitive, and String types, we convert the string value into the corresponding type
// For object type, we assume the string value is a Json string, and try to use Gson to deserialize into the
// given field type.
Class rawType = fieldType.getRawType();
if (String.class.equals(rawType)) {
return value;
}
if (rawType.isPrimitive()) {
rawType = Primitives.wrap(rawType);
}
if (Character.class.equals(rawType)) {
if (value.length() != 1) {
throw new IllegalArgumentException(
String.format("Property of type char is not length 1: '%s'", value));
} else {
return value.charAt(0);
}
}
// discard decimal point for all non floating point data types
if (Long.class.equals(rawType) || Short.class.equals(rawType)
|| Integer.class.equals(rawType) || Byte.class.equals(rawType)) {
if (value.endsWith(".0")) {
value = value.substring(0, value.lastIndexOf("."));
}
}
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
String errorMessage = Strings.isNullOrEmpty(value)
? String.format("Value of field %s.%s is null or empty. It should be a number",
declareType, name) :
String.format("Value of field %s.%s is expected to be a number", declareType, name);
throw new InvalidPluginConfigException(errorMessage, e.getCause());
}
throw e;
}
}
try {
// Assuming it is a POJO type, use Gson to deserialize the value
return gson.fromJson(value, fieldType.getType());
} catch (JsonSyntaxException e) {
throw new InvalidPluginConfigException(
String.format("Failed to assign value '%s' to plugin config field %s.%s", value,
declareType, name), e);
}
}
}
}