From 3ce5b53507312d06fe2edc90e64fe8224b357978 Mon Sep 17 00:00:00 2001 From: David Venable Date: Wed, 24 May 2023 16:22:53 -0500 Subject: [PATCH] Data Prepper Extensions #2636, #2637 (#2730) Data Prepper Extensions #2636, #2637 Initial work supports the basic model and the ability to inject shared objects across plugins. Signed-off-by: David Venable --------- Signed-off-by: David Venable --- .../model/plugin/ExtensionPlugin.java | 23 +++ .../model/plugin/ExtensionPoints.java | 23 +++ .../model/plugin/ExtensionProvider.java | 44 +++++ data-prepper-core/build.gradle | 1 + .../ClasspathExtensionClassProvider.java | 76 +++++++ .../ComponentPluginArgumentsContext.java | 157 +++++++++++++++ .../plugin/DataPrepperExtensionPoints.java | 41 ++++ .../plugin/DefaultPluginFactory.java | 10 +- .../plugin/ExtensionClassProvider.java | 14 ++ .../dataprepper/plugin/ExtensionLoader.java | 63 ++++++ .../dataprepper/plugin/ExtensionsApplier.java | 36 ++++ .../plugin/PluginArgumentsContext.java | 150 +------------- .../plugin/PluginBeanFactoryProvider.java | 11 +- .../dataprepper/plugin/PluginCreator.java | 2 +- .../ClasspathExtensionClassProviderTest.java | 90 +++++++++ ... ComponentPluginArgumentsContextTest.java} | 24 +-- .../DataPrepperExtensionPointsTest.java | 124 ++++++++++++ .../plugin/DefaultPluginFactoryTest.java | 16 +- .../plugin/ExtensionLoaderTest.java | 187 ++++++++++++++++++ .../plugin/ExtensionsApplierTest.java | 58 ++++++ .../dataprepper/plugin/ExtensionsIT.java | 132 +++++++++++++ .../plugin/PluginBeanFactoryProviderTest.java | 47 ++++- .../dataprepper/plugin/PluginCreatorTest.java | 4 +- .../plugins/TestPluginUsingExtension.java | 36 ++++ .../plugins/test/TestExtension.java | 69 +++++++ 25 files changed, 1253 insertions(+), 185 deletions(-) create mode 100644 data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPlugin.java create mode 100644 data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPoints.java create mode 100644 data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionProvider.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProvider.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContext.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPoints.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionClassProvider.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionLoader.java create mode 100644 data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionsApplier.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProviderTest.java rename data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/{PluginArgumentsContextTest.java => ComponentPluginArgumentsContextTest.java} (84%) create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPointsTest.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionLoaderTest.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsApplierTest.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsIT.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestPluginUsingExtension.java create mode 100644 data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/test/TestExtension.java diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPlugin.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPlugin.java new file mode 100644 index 0000000000..9edf55c454 --- /dev/null +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPlugin.java @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.model.plugin; + +/** + * The interface to implement to become an extension. + * + * @since 2.3 + */ +public interface ExtensionPlugin { + /** + * Register your extension with the available {@link ExtensionPoints} provided + * by Data Prepper. + *

+ * Each extension will have this method called once on start-up. + * + * @param extensionPoints The {@link ExtensionPoints} wherein the extension can extend behaviors. + */ + void apply(ExtensionPoints extensionPoints); +} diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPoints.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPoints.java new file mode 100644 index 0000000000..7f515806b7 --- /dev/null +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPoints.java @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.model.plugin; + +/** + * A model for extending Data Prepper. A Data Prepper extension will call methods in a provided instance + * of this class. + * + * @since 2.3 + */ +public interface ExtensionPoints { + /** + * Adds an {@link ExtensionProvider} to Data Prepper. This allows an extension to make a class + * available to plugins within Data Prepper. + * + * @param extensionProvider The {@link ExtensionProvider} which this extension is creating. + * @since 2.3 + */ + void addExtensionProvider(ExtensionProvider extensionProvider); +} diff --git a/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionProvider.java b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionProvider.java new file mode 100644 index 0000000000..45ab3682fd --- /dev/null +++ b/data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.model.plugin; + +import java.util.Optional; + +/** + * An interface to be provided by extensions which wish to provide classes to plugins. + * + * @param The type of class provided. + * @since 2.3 + */ +public interface ExtensionProvider { + /** + * Returns an instance of the class being provided. + *

+ * This is called everytime a plugin requires an instance. The implementor can re-use + * instances, or create them on-demand depending on the intention of the extension + * author. + * + * @param context The context for the request. This is currently a placeholder. + * @return An instance as requested. + */ + Optional provideInstance(Context context); + + /** + * Returns the Java {@link Class} which this extension is providing. + * + * @return A {@link Class}. + */ + Class supportedClass(); + + /** + * The context for creating a new instance. + * + * @since 2.3 + */ + interface Context { + + } +} diff --git a/data-prepper-core/build.gradle b/data-prepper-core/build.gradle index 555ba47f37..e779c8977e 100644 --- a/data-prepper-core/build.gradle +++ b/data-prepper-core/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-core' implementation 'org.apache.logging.log4j:log4j-slf4j2-impl' implementation 'javax.inject:javax.inject:1' + implementation 'javax.annotation:javax.annotation-api:1.3.2' implementation(libs.spring.core) { exclude group: 'commons-logging', module: 'commons-logging' } diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProvider.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProvider.java new file mode 100644 index 0000000000..2b83842988 --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; +import org.reflections.Reflections; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Implements {@link ExtensionClassProvider} using the classpath to detect extensions. + * This uses the same {@link PluginPackagesSupplier} as {@link ClasspathPluginProvider}. + */ +@Named +public class ClasspathExtensionClassProvider implements ExtensionClassProvider { + private static final Logger LOG = LoggerFactory.getLogger(ClasspathExtensionClassProvider.class); + private final Reflections reflections; + private Set> extensionPluginClasses; + + @Inject + public ClasspathExtensionClassProvider() { + this(createReflections()); + } + + private static Reflections createReflections() { + final String[] packages = new PluginPackagesSupplier().get(); + FilterBuilder filterBuilder = new FilterBuilder(); + for (String packageToInclude : packages) { + filterBuilder = filterBuilder.includePackage(packageToInclude); + } + + return new Reflections(new ConfigurationBuilder() + .forPackages(packages) + .filterInputsBy(filterBuilder)); + } + + /** + * For testing purposes. + * + * @param reflections A {@link Reflections} object. + */ + ClasspathExtensionClassProvider(final Reflections reflections) { + this.reflections = reflections; + } + + @Override + public Collection> loadExtensionPluginClasses() { + if (extensionPluginClasses == null) { + extensionPluginClasses = scanForExtensionPlugins(); + } + return extensionPluginClasses; + } + + private Set> scanForExtensionPlugins() { + final Set> extensionClasses = reflections.getSubTypesOf(ExtensionPlugin.class); + + if (LOG.isDebugEnabled()) { + LOG.debug("Found {} extension classes.", extensionClasses.size()); + LOG.debug("Extensions classes: {}", + extensionClasses.stream().map(Class::getName).collect(Collectors.joining(", "))); + } + + return extensionClasses; + } +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContext.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContext.java new file mode 100644 index 0000000000..cb809f75b7 --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContext.java @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.metrics.PluginMetrics; +import org.opensearch.dataprepper.model.configuration.PipelineDescription; +import org.opensearch.dataprepper.model.configuration.PluginSetting; +import org.opensearch.dataprepper.model.plugin.InvalidPluginDefinitionException; +import org.opensearch.dataprepper.model.plugin.PluginFactory; +import org.opensearch.dataprepper.model.event.EventFactory; +import org.opensearch.dataprepper.model.acknowledgements.AcknowledgementSetManager; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * An internal class which represents all the data which can be provided + * when constructing a new plugin. + */ +class ComponentPluginArgumentsContext implements PluginArgumentsContext { + private static final String UNABLE_TO_CREATE_PLUGIN_PARAMETER = "Unable to create an argument for required plugin parameter type: "; + private final Map, Supplier> typedArgumentsSuppliers; + + @Nullable + private final BeanFactory beanFactory; + + private ComponentPluginArgumentsContext(final Builder builder) { + Objects.requireNonNull(builder.pluginSetting, + "PluginArgumentsContext received a null Builder object. This is likely an error in the plugin framework."); + + beanFactory = builder.beanFactory; + + typedArgumentsSuppliers = new HashMap<>(); + + typedArgumentsSuppliers.put(PluginSetting.class, () -> builder.pluginSetting); + + if(builder.pluginConfiguration != null) { + typedArgumentsSuppliers.put(builder.pluginConfiguration.getClass(), () -> builder.pluginConfiguration); + } + + typedArgumentsSuppliers.put(PluginMetrics.class, () -> PluginMetrics.fromPluginSetting(builder.pluginSetting)); + + if (builder.pipelineDescription != null) { + typedArgumentsSuppliers.put(PipelineDescription.class, () -> builder.pipelineDescription); + } + + if (builder.pluginFactory != null) { + typedArgumentsSuppliers.put(PluginFactory.class, () -> builder.pluginFactory); + } + + if (builder.eventFactory != null) { + typedArgumentsSuppliers.put(EventFactory.class, () -> builder.eventFactory); + } + + if (builder.acknowledgementSetManager != null) { + typedArgumentsSuppliers.put(AcknowledgementSetManager.class, () -> builder.acknowledgementSetManager); + } + } + + @Override + public Object[] createArguments(final Class[] parameterTypes) { + return Arrays.stream(parameterTypes) + .map(this::getRequiredArgumentSupplier) + .map(Supplier::get) + .toArray(); + } + + private Supplier getRequiredArgumentSupplier(final Class parameterType) { + if(typedArgumentsSuppliers.containsKey(parameterType)) { + return typedArgumentsSuppliers.get(parameterType); + } + else if (beanFactory != null) { + return createBeanSupplier(parameterType, beanFactory); + } + else { + throw new InvalidPluginDefinitionException(UNABLE_TO_CREATE_PLUGIN_PARAMETER + parameterType); + } + } + + /** + * @since 1.3 + * + * Create a supplier to return a bean of type
parameterType
if one is available in
beanFactory
+ * + * @param parameterType type of bean requested + * @param beanFactory bean source the generated supplier will use + * @return supplier of object type bean + * @throws InvalidPluginDefinitionException if no bean is available from beanFactory + */ + private Supplier createBeanSupplier(final Class parameterType, final BeanFactory beanFactory) { + return () -> { + try { + return beanFactory.getBean(parameterType); + } catch (final BeansException e) { + throw new InvalidPluginDefinitionException(UNABLE_TO_CREATE_PLUGIN_PARAMETER + parameterType, e); + } + }; + } + + static class Builder { + private Object pluginConfiguration; + private PluginSetting pluginSetting; + private PluginFactory pluginFactory; + private PipelineDescription pipelineDescription; + private BeanFactory beanFactory; + private EventFactory eventFactory; + private AcknowledgementSetManager acknowledgementSetManager; + + Builder withPluginConfiguration(final Object pluginConfiguration) { + this.pluginConfiguration = pluginConfiguration; + return this; + } + + Builder withPluginSetting(final PluginSetting pluginSetting) { + this.pluginSetting = pluginSetting; + return this; + } + + Builder withEventFactory(final EventFactory eventFactory) { + this.eventFactory = eventFactory; + return this; + } + + Builder withAcknowledgementSetManager(final AcknowledgementSetManager acknowledgementSetManager) { + this.acknowledgementSetManager = acknowledgementSetManager; + return this; + } + + Builder withPluginFactory(final PluginFactory pluginFactory) { + this.pluginFactory = pluginFactory; + return this; + } + + Builder withPipelineDescription(final PipelineDescription pipelineDescription) { + this.pipelineDescription = pipelineDescription; + return this; + } + + Builder withBeanFactory(final BeanFactory beanFactory) { + this.beanFactory = beanFactory; + return this; + } + + ComponentPluginArgumentsContext build() { + return new ComponentPluginArgumentsContext(this); + } + } +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPoints.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPoints.java new file mode 100644 index 0000000000..64b712cf24 --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPoints.java @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.plugin.ExtensionPoints; +import org.opensearch.dataprepper.model.plugin.ExtensionProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.support.GenericApplicationContext; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Objects; + +@Named +public class DataPrepperExtensionPoints implements ExtensionPoints { + private static final ExtensionProvider.Context EMPTY_CONTEXT = new EmptyContext(); + private final GenericApplicationContext sharedApplicationContext; + + @Inject + public DataPrepperExtensionPoints( + final PluginBeanFactoryProvider pluginBeanFactoryProvider) { + Objects.requireNonNull(pluginBeanFactoryProvider); + Objects.requireNonNull(pluginBeanFactoryProvider.getSharedPluginApplicationContext()); + this.sharedApplicationContext = pluginBeanFactoryProvider.getSharedPluginApplicationContext(); + } + + @Override + public void addExtensionProvider(final ExtensionProvider extensionProvider) { + sharedApplicationContext.registerBean( + extensionProvider.supportedClass(), + () -> extensionProvider.provideInstance(EMPTY_CONTEXT), + b -> b.setScope(BeanDefinition.SCOPE_PROTOTYPE)); + } + + private static class EmptyContext implements ExtensionProvider.Context { + + } +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java index ebdc505e57..e21acc8ec9 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/DefaultPluginFactory.java @@ -13,6 +13,7 @@ import org.opensearch.dataprepper.acknowledgements.DefaultAcknowledgementSetManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.DependsOn; import javax.inject.Inject; import javax.inject.Named; @@ -29,6 +30,7 @@ * @since 1.2 */ @Named +@DependsOn({"extensionsApplier"}) public class DefaultPluginFactory implements PluginFactory { private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginFactory.class); @@ -68,7 +70,7 @@ public T loadPlugin(final Class baseClass, final PluginSetting pluginSett final String pluginName = pluginSetting.getName(); final Class pluginClass = getPluginClass(baseClass, pluginName); - final PluginArgumentsContext constructionContext = getConstructionContext(pluginSetting, pluginClass); + final ComponentPluginArgumentsContext constructionContext = getConstructionContext(pluginSetting, pluginClass); return pluginCreator.newPluginInstance(pluginClass, constructionContext, pluginName); } @@ -86,7 +88,7 @@ public List loadPlugins( if(numberOfInstances == null || numberOfInstances < 0) throw new IllegalArgumentException("The numberOfInstances must be provided as a non-negative integer."); - final PluginArgumentsContext constructionContext = getConstructionContext(pluginSetting, pluginClass); + final ComponentPluginArgumentsContext constructionContext = getConstructionContext(pluginSetting, pluginClass); final List plugins = new ArrayList<>(numberOfInstances); for (int i = 0; i < numberOfInstances; i++) { @@ -95,13 +97,13 @@ public List loadPlugins( return plugins; } - private PluginArgumentsContext getConstructionContext(final PluginSetting pluginSetting, final Class pluginClass) { + private ComponentPluginArgumentsContext getConstructionContext(final PluginSetting pluginSetting, final Class pluginClass) { final DataPrepperPlugin pluginAnnotation = pluginClass.getAnnotation(DataPrepperPlugin.class); final Class pluginConfigurationType = pluginAnnotation.pluginConfigurationType(); final Object configuration = pluginConfigurationConverter.convert(pluginConfigurationType, pluginSetting); - return new PluginArgumentsContext.Builder() + return new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .withPipelineDescription(pluginSetting) .withPluginConfiguration(configuration) diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionClassProvider.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionClassProvider.java new file mode 100644 index 0000000000..fb4abbfccb --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionClassProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; + +import java.util.Collection; + +interface ExtensionClassProvider { + Collection> loadExtensionPluginClasses(); +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionLoader.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionLoader.java new file mode 100644 index 0000000000..20b6690552 --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionLoader.java @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; +import org.opensearch.dataprepper.model.plugin.InvalidPluginDefinitionException; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Named +public class ExtensionLoader { + private final ExtensionClassProvider extensionClassProvider; + private final PluginCreator pluginCreator; + + @Inject + ExtensionLoader( + final ExtensionClassProvider extensionClassProvider, + final PluginCreator pluginCreator) { + this.extensionClassProvider = extensionClassProvider; + this.pluginCreator = pluginCreator; + } + + List loadExtensions() { + final PluginArgumentsContext pluginArgumentsContext = new NoArgumentsArgumentsContext(); + + return extensionClassProvider.loadExtensionPluginClasses() + .stream() + .map(extensionClass -> pluginCreator.newPluginInstance(extensionClass, pluginArgumentsContext, convertClassToName(extensionClass))) + .collect(Collectors.toList()); + } + + private String convertClassToName(final Class extensionClass) { + final String className = extensionClass.getSimpleName(); + return classNameToPluginName(className); + } + + static String classNameToPluginName(final String className) { + + final String[] words = className.split("(?=\\p{Upper})"); + + return Arrays.stream(words) + .map(String::toLowerCase) + .collect(Collectors.joining("_")) + .replace("$", ""); + } + + private static class NoArgumentsArgumentsContext implements PluginArgumentsContext { + @Override + public Object[] createArguments(final Class[] parameterTypes) { + if(parameterTypes.length != 0) { + throw new InvalidPluginDefinitionException("No arguments are permitted for extensions constructors."); + } + return new Object[0]; + } + } +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionsApplier.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionsApplier.java new file mode 100644 index 0000000000..b2bd9ffa6f --- /dev/null +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/ExtensionsApplier.java @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.List; + +@Named("extensionsApplier") +class ExtensionsApplier { + private final DataPrepperExtensionPoints dataPrepperExtensionPoints; + private final ExtensionLoader extensionLoader; + + @Inject + ExtensionsApplier( + final DataPrepperExtensionPoints dataPrepperExtensionPoints, + final ExtensionLoader extensionLoader) { + this.dataPrepperExtensionPoints = dataPrepperExtensionPoints; + this.extensionLoader = extensionLoader; + } + + @PostConstruct + void applyExtensions() { + final List extensionPlugins = extensionLoader.loadExtensions(); + + for (ExtensionPlugin extensionPlugin : extensionPlugins) { + extensionPlugin.apply(dataPrepperExtensionPoints); + } + } +} diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginArgumentsContext.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginArgumentsContext.java index 2f4ec49c74..b7736ce52a 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginArgumentsContext.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginArgumentsContext.java @@ -5,152 +5,6 @@ package org.opensearch.dataprepper.plugin; -import org.opensearch.dataprepper.metrics.PluginMetrics; -import org.opensearch.dataprepper.model.configuration.PipelineDescription; -import org.opensearch.dataprepper.model.configuration.PluginSetting; -import org.opensearch.dataprepper.model.plugin.InvalidPluginDefinitionException; -import org.opensearch.dataprepper.model.plugin.PluginFactory; -import org.opensearch.dataprepper.model.event.EventFactory; -import org.opensearch.dataprepper.model.acknowledgements.AcknowledgementSetManager; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; - -import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Supplier; - -/** - * An internal class which represents all the data which can be provided - * when constructing a new plugin. - */ -class PluginArgumentsContext { - private static final String UNABLE_TO_CREATE_PLUGIN_PARAMETER = "Unable to create an argument for required plugin parameter type: "; - private final Map, Supplier> typedArgumentsSuppliers; - - @Nullable - private final BeanFactory beanFactory; - - private PluginArgumentsContext(final Builder builder) { - Objects.requireNonNull(builder.pluginSetting, - "PluginArgumentsContext received a null Builder object. This is likely an error in the plugin framework."); - - beanFactory = builder.beanFactory; - - typedArgumentsSuppliers = new HashMap<>(); - - typedArgumentsSuppliers.put(PluginSetting.class, () -> builder.pluginSetting); - - if(builder.pluginConfiguration != null) { - typedArgumentsSuppliers.put(builder.pluginConfiguration.getClass(), () -> builder.pluginConfiguration); - } - - typedArgumentsSuppliers.put(PluginMetrics.class, () -> PluginMetrics.fromPluginSetting(builder.pluginSetting)); - - if (builder.pipelineDescription != null) { - typedArgumentsSuppliers.put(PipelineDescription.class, () -> builder.pipelineDescription); - } - - if (builder.pluginFactory != null) { - typedArgumentsSuppliers.put(PluginFactory.class, () -> builder.pluginFactory); - } - - if (builder.eventFactory != null) { - typedArgumentsSuppliers.put(EventFactory.class, () -> builder.eventFactory); - } - - if (builder.acknowledgementSetManager != null) { - typedArgumentsSuppliers.put(AcknowledgementSetManager.class, () -> builder.acknowledgementSetManager); - } - } - - Object[] createArguments(final Class[] parameterTypes) { - return Arrays.stream(parameterTypes) - .map(this::getRequiredArgumentSupplier) - .map(Supplier::get) - .toArray(); - } - - private Supplier getRequiredArgumentSupplier(final Class parameterType) { - if(typedArgumentsSuppliers.containsKey(parameterType)) { - return typedArgumentsSuppliers.get(parameterType); - } - else if (beanFactory != null) { - return createBeanSupplier(parameterType, beanFactory); - } - else { - throw new InvalidPluginDefinitionException(UNABLE_TO_CREATE_PLUGIN_PARAMETER + parameterType); - } - } - - /** - * @since 1.3 - * - * Create a supplier to return a bean of type
parameterType
if one is available in
beanFactory
- * - * @param parameterType type of bean requested - * @param beanFactory bean source the generated supplier will use - * @return supplier of object type bean - * @throws InvalidPluginDefinitionException if no bean is available from beanFactory - */ - private Supplier createBeanSupplier(final Class parameterType, final BeanFactory beanFactory) { - return () -> { - try { - return beanFactory.getBean(parameterType); - } catch (final BeansException e) { - throw new InvalidPluginDefinitionException(UNABLE_TO_CREATE_PLUGIN_PARAMETER + parameterType, e); - } - }; - } - - static class Builder { - private Object pluginConfiguration; - private PluginSetting pluginSetting; - private PluginFactory pluginFactory; - private PipelineDescription pipelineDescription; - private BeanFactory beanFactory; - private EventFactory eventFactory; - private AcknowledgementSetManager acknowledgementSetManager; - - Builder withPluginConfiguration(final Object pluginConfiguration) { - this.pluginConfiguration = pluginConfiguration; - return this; - } - - Builder withPluginSetting(final PluginSetting pluginSetting) { - this.pluginSetting = pluginSetting; - return this; - } - - Builder withEventFactory(final EventFactory eventFactory) { - this.eventFactory = eventFactory; - return this; - } - - Builder withAcknowledgementSetManager(final AcknowledgementSetManager acknowledgementSetManager) { - this.acknowledgementSetManager = acknowledgementSetManager; - return this; - } - - Builder withPluginFactory(final PluginFactory pluginFactory) { - this.pluginFactory = pluginFactory; - return this; - } - - Builder withPipelineDescription(final PipelineDescription pipelineDescription) { - this.pipelineDescription = pipelineDescription; - return this; - } - - Builder withBeanFactory(final BeanFactory beanFactory) { - this.beanFactory = beanFactory; - return this; - } - - PluginArgumentsContext build() { - return new PluginArgumentsContext(this); - } - } +interface PluginArgumentsContext { + Object[] createArguments(final Class[] parameterTypes); } diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProvider.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProvider.java index 12f9256de8..a0e03cd16b 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProvider.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProvider.java @@ -26,7 +26,7 @@ */ @Named class PluginBeanFactoryProvider implements Provider { - private final ApplicationContext sharedPluginApplicationContext; + private final GenericApplicationContext sharedPluginApplicationContext; @Inject PluginBeanFactoryProvider(final ApplicationContext coreContext) { @@ -34,6 +34,15 @@ class PluginBeanFactoryProvider implements Provider { sharedPluginApplicationContext = new GenericApplicationContext(publicContext); } + /** + * Provides an {@link GenericApplicationContext} which is shared across all plugins. + * + * @return The shared application context. + */ + GenericApplicationContext getSharedPluginApplicationContext() { + return sharedPluginApplicationContext; + } + /** * @since 1.3 * Creates a new isolated application context that inherits from diff --git a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginCreator.java b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginCreator.java index 9c59e02148..df6e223ef0 100644 --- a/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginCreator.java +++ b/data-prepper-core/src/main/java/org/opensearch/dataprepper/plugin/PluginCreator.java @@ -73,7 +73,7 @@ private Constructor getConstructor(final Class pluginClass, final Stri final String error = String.format("Data Prepper plugin %s with name %s does not have a valid plugin constructor. " + "Please ensure the plugin has a constructor that either: " + - "1. Is annotated with @DataPrepperPlugin; " + + "1. Is annotated with @DataPrepperPluginConstructor; " + "2. Contains a single argument of type PluginSetting; or " + "3. Is the default constructor.", pluginClass.getSimpleName(), pluginName); diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProviderTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProviderTest.java new file mode 100644 index 0000000000..6770090128 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ClasspathExtensionClassProviderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; +import org.reflections.Reflections; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +class ClasspathExtensionClassProviderTest { + private Reflections reflections; + + @BeforeEach + void setUp() { + reflections = mock(Reflections.class); + } + + private ClasspathExtensionClassProvider createObjectUnderTest() { + return new ClasspathExtensionClassProvider(reflections); + } + + @Test + void loadExtensionPluginClasses_should_scan_for_plugins() { + final ClasspathExtensionClassProvider objectUnderTest = createObjectUnderTest(); + + then(reflections).shouldHaveNoInteractions(); + + given(reflections.getSubTypesOf(ExtensionPlugin.class)) + .willReturn(Collections.emptySet()); + + objectUnderTest.loadExtensionPluginClasses(); + + then(reflections) + .should() + .getSubTypesOf(ExtensionPlugin.class); + } + + @Test + void loadExtensionPluginClasses_should_scan_for_plugins_only_once() { + final ClasspathExtensionClassProvider objectUnderTest = createObjectUnderTest(); + + given(reflections.getSubTypesOf(ExtensionPlugin.class)) + .willReturn(Collections.emptySet()); + + for (int i = 0; i < 10; i++) + objectUnderTest.loadExtensionPluginClasses(); + + then(reflections) + .should() + .getSubTypesOf(ExtensionPlugin.class); + } + + @Test + void findPluginExtensionClass_should_return_empty_if_no_plugins_found() { + given(reflections.getSubTypesOf(ExtensionPlugin.class)) + .willReturn(Collections.emptySet()); + + final Collection> extensionPluginClasses = createObjectUnderTest().loadExtensionPluginClasses(); + assertThat(extensionPluginClasses, notNullValue()); + assertThat(extensionPluginClasses.size(), equalTo(0)); + } + + @Test + void findPluginExtensionClass_should_return_all_plugin_classes_found() { + final Set> classes = Set.of( + mock(ExtensionPlugin.class).getClass() + ); + given(reflections.getSubTypesOf(ExtensionPlugin.class)) + .willReturn(classes); + + final Collection> extensionPluginClasses = createObjectUnderTest().loadExtensionPluginClasses(); + assertThat(extensionPluginClasses, notNullValue()); + assertThat(extensionPluginClasses.size(), equalTo(classes.size())); + assertThat(extensionPluginClasses, equalTo(classes)); + } +} \ No newline at end of file diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginArgumentsContextTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContextTest.java similarity index 84% rename from data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginArgumentsContextTest.java rename to data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContextTest.java index 2c975e25e7..51506d4c23 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginArgumentsContextTest.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ComponentPluginArgumentsContextTest.java @@ -32,7 +32,7 @@ import static org.mockito.Mockito.mockStatic; @ExtendWith(MockitoExtension.class) -class PluginArgumentsContextTest { +class ComponentPluginArgumentsContextTest { @Mock private PluginSetting pluginSetting; @@ -53,7 +53,7 @@ public SubPluginSetting(final String name, final Map settings) { @Test void createArguments_with_unavailable_argument_should_throw() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .build(); @@ -63,7 +63,7 @@ void createArguments_with_unavailable_argument_should_throw() { @Test void createArguments_with_single_class() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginConfiguration(testPluginConfiguration) .withPluginSetting(pluginSetting) .build(); @@ -75,7 +75,7 @@ void createArguments_with_single_class() { @Test void createArguments_with_single_class_when_PluginSetting_is_inherited() { pluginSetting = mock(SubPluginSetting.class); - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginConfiguration(testPluginConfiguration) .withPluginSetting(pluginSetting) .build(); @@ -89,7 +89,7 @@ void createArguments_with_single_class_using_bean_factory() { final Object mock = mock(Object.class); doReturn(mock).when(beanFactory).getBean(eq(Object.class)); - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .withBeanFactory(beanFactory) .build(); @@ -102,7 +102,7 @@ void createArguments_with_single_class_using_bean_factory() { void createArguments_given_bean_not_available_with_single_class_using_bean_factory() { doThrow(mock(BeansException.class)).when(beanFactory).getBean((Class) any()); - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .withBeanFactory(beanFactory) .build(); @@ -119,7 +119,7 @@ void createArguments_with_multiple_supplier_sources() { final Object mock = mock(Object.class); doReturn(mock).when(beanFactory).getBean(eq(Object.class)); - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .withPluginConfiguration(testPluginConfiguration) .withBeanFactory(beanFactory) @@ -131,7 +131,7 @@ void createArguments_with_multiple_supplier_sources() { @Test void createArguments_with_two_classes() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginConfiguration(testPluginConfiguration) .withPluginSetting(pluginSetting) .build(); @@ -142,7 +142,7 @@ void createArguments_with_two_classes() { @Test void createArguments_with_two_classes_inverted_order() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginConfiguration(testPluginConfiguration) .withPluginSetting(pluginSetting) .build(); @@ -153,7 +153,7 @@ void createArguments_with_two_classes_inverted_order() { @Test void createArguments_with_three_classes() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginConfiguration(testPluginConfiguration) .withPluginSetting(pluginSetting) .withPipelineDescription(pluginSetting) @@ -166,7 +166,7 @@ void createArguments_with_three_classes() { @Test void createArguments_with_pluginFactory_should_return_the_instance_from_the_builder() { final PluginFactory pluginFactory = mock(PluginFactory.class); - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .withPluginFactory(pluginFactory) .build(); @@ -177,7 +177,7 @@ void createArguments_with_pluginFactory_should_return_the_instance_from_the_buil @Test void createArguments_with_PluginMetrics() { - final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder() + final ComponentPluginArgumentsContext objectUnderTest = new ComponentPluginArgumentsContext.Builder() .withPluginSetting(pluginSetting) .build(); diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPointsTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPointsTest.java new file mode 100644 index 0000000000..7647f8a0d1 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPointsTest.java @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.dataprepper.model.plugin.ExtensionProvider; +import org.opensearch.dataprepper.plugins.test.TestExtension; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.context.support.GenericApplicationContext; + +import java.util.Optional; +import java.util.function.Supplier; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DataPrepperExtensionPointsTest { + @Mock(lenient = true) + private PluginBeanFactoryProvider pluginBeanFactoryProvider; + + @Mock(lenient = true) + private GenericApplicationContext sharedApplicationContext; + + @Mock(lenient = true) + private ExtensionProvider extensionProvider; + + private Class extensionClass; + + @BeforeEach + void setUp() { + when(pluginBeanFactoryProvider.getSharedPluginApplicationContext()) + .thenReturn(sharedApplicationContext); + + extensionClass = TestExtension.TestModel.class; + + when(extensionProvider.supportedClass()).thenReturn(extensionClass); + } + + private DataPrepperExtensionPoints createObjectUnderTest() { + return new DataPrepperExtensionPoints(pluginBeanFactoryProvider); + } + + @Test + void constructor_throws_if_provider_is_null() { + pluginBeanFactoryProvider = null; + + assertThrows(NullPointerException.class, this::createObjectUnderTest); + } + + @Test + void constructor_throws_if_provider_getSharedPluginApplicationContext_is_null() { + reset(pluginBeanFactoryProvider); + + assertThrows(NullPointerException.class, this::createObjectUnderTest); + } + + @Test + void addExtensionProvider_should_registerBean() { + createObjectUnderTest().addExtensionProvider(extensionProvider); + + verify(sharedApplicationContext).registerBean(eq(extensionClass), any(Supplier.class), any(BeanDefinitionCustomizer.class)); + } + + @Test + void addExtensionProvider_should_registerBean_which_calls_provideInstance() { + createObjectUnderTest().addExtensionProvider(extensionProvider); + + final ArgumentCaptor> supplierArgumentCaptor = + ArgumentCaptor.forClass(Supplier.class); + + verify(sharedApplicationContext).registerBean(eq(extensionClass), supplierArgumentCaptor.capture(), any(BeanDefinitionCustomizer.class)); + + final Supplier extensionProviderSupplier = supplierArgumentCaptor.getValue(); + + verify(extensionProvider, never()).provideInstance(any()); + + Object providedInstance = mock(Object.class); + when(extensionProvider.provideInstance(any())).thenReturn(Optional.of(providedInstance)); + final Object actualValueFromSupplier = extensionProviderSupplier.get(); + + assertThat(actualValueFromSupplier, instanceOf(Optional.class)); + Optional optionalWrapper = (Optional) actualValueFromSupplier; + assertThat(optionalWrapper.isPresent(), equalTo(true)); + final Object actualInstance = optionalWrapper.get(); + assertThat(actualInstance, equalTo(providedInstance)); + verify(extensionProvider).provideInstance(any()); + } + + @Test + void addExtensionProvider_should_registerBean_as_prototype() { + createObjectUnderTest().addExtensionProvider(extensionProvider); + + final ArgumentCaptor beanDefinitionCustomizerArgumentCaptor = + ArgumentCaptor.forClass(BeanDefinitionCustomizer.class); + + verify(sharedApplicationContext).registerBean(eq(extensionClass), any(Supplier.class), beanDefinitionCustomizerArgumentCaptor.capture()); + + final BeanDefinitionCustomizer beanDefinitionCustomizer = beanDefinitionCustomizerArgumentCaptor.getValue(); + + final BeanDefinition beanDefinition = mock(BeanDefinition.class); + beanDefinitionCustomizer.customize(beanDefinition); + + verify(beanDefinition).setScope(BeanDefinition.SCOPE_PROTOTYPE); + } +} diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java index 99cf7befc7..3a5b1f3bdd 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/DefaultPluginFactoryTest.java @@ -176,7 +176,7 @@ void loadPlugin_should_create_a_new_instance_of_the_first_plugin_found() { final Object convertedConfiguration = mock(Object.class); given(pluginConfigurationConverter.convert(PluginSetting.class, pluginSetting)) .willReturn(convertedConfiguration); - given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(PluginArgumentsContext.class), eq(pluginName))) + given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(ComponentPluginArgumentsContext.class), eq(pluginName))) .willReturn(expectedInstance); assertThat(createObjectUnderTest().loadPlugin(baseClass, pluginSetting), @@ -225,16 +225,16 @@ void loadPlugins_should_return_a_single_instance_when_the_the_numberOfInstances_ final Object convertedConfiguration = mock(Object.class); given(pluginConfigurationConverter.convert(PluginSetting.class, pluginSetting)) .willReturn(convertedConfiguration); - given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(PluginArgumentsContext.class), eq(pluginName))) + given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(ComponentPluginArgumentsContext.class), eq(pluginName))) .willReturn(expectedInstance); final List plugins = createObjectUnderTest().loadPlugins( baseClass, pluginSetting, c -> 1); verify(beanFactoryProvider).get(); - final ArgumentCaptor pluginArgumentsContextArgCapture = ArgumentCaptor.forClass(PluginArgumentsContext.class); + final ArgumentCaptor pluginArgumentsContextArgCapture = ArgumentCaptor.forClass(ComponentPluginArgumentsContext.class); verify(pluginCreator).newPluginInstance(eq(expectedPluginClass), pluginArgumentsContextArgCapture.capture(), eq(pluginName)); - final PluginArgumentsContext actualPluginArgumentsContext = pluginArgumentsContextArgCapture.getValue(); + final ComponentPluginArgumentsContext actualPluginArgumentsContext = pluginArgumentsContextArgCapture.getValue(); final List classes = List.of(PipelineDescription.class); final Object[] pipelineDescriptionObj = actualPluginArgumentsContext.createArguments(classes.toArray(new Class[1])); assertThat(pipelineDescriptionObj.length, equalTo(1)); @@ -254,7 +254,7 @@ void loadPlugins_should_return_an_instance_for_the_total_count() { final Object convertedConfiguration = mock(Object.class); given(pluginConfigurationConverter.convert(PluginSetting.class, pluginSetting)) .willReturn(convertedConfiguration); - given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(PluginArgumentsContext.class), eq(pluginName))) + given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(ComponentPluginArgumentsContext.class), eq(pluginName))) .willReturn(expectedInstance1) .willReturn(expectedInstance2) .willReturn(expectedInstance3); @@ -266,9 +266,9 @@ void loadPlugins_should_return_an_instance_for_the_total_count() { baseClass, pluginSetting, c -> 3); verify(beanFactoryProvider).get(); - final ArgumentCaptor pluginArgumentsContextArgCapture = ArgumentCaptor.forClass(PluginArgumentsContext.class); + final ArgumentCaptor pluginArgumentsContextArgCapture = ArgumentCaptor.forClass(ComponentPluginArgumentsContext.class); verify(pluginCreator, times(3)).newPluginInstance(eq(expectedPluginClass), pluginArgumentsContextArgCapture.capture(), eq(pluginName)); - final List actualPluginArgumentsContextList = pluginArgumentsContextArgCapture.getAllValues(); + final List actualPluginArgumentsContextList = pluginArgumentsContextArgCapture.getAllValues(); assertThat(actualPluginArgumentsContextList.size(), equalTo(3)); actualPluginArgumentsContextList.forEach(pluginArgumentsContext -> { final List classes = List.of(PipelineDescription.class); @@ -306,7 +306,7 @@ void loadPlugin_should_create_a_new_instance_of_the_first_plugin_found_with_corr final Object convertedConfiguration = mock(Object.class); given(pluginConfigurationConverter.convert(PluginSetting.class, pluginSetting)) .willReturn(convertedConfiguration); - given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(PluginArgumentsContext.class), eq(TEST_SINK_DEPRECATED_NAME))) + given(pluginCreator.newPluginInstance(eq(expectedPluginClass), any(ComponentPluginArgumentsContext.class), eq(TEST_SINK_DEPRECATED_NAME))) .willReturn(expectedInstance); assertThat(createObjectUnderTest().loadPlugin(baseClass, pluginSetting), equalTo(expectedInstance)); diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionLoaderTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionLoaderTest.java new file mode 100644 index 0000000000..00514f4e3e --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionLoaderTest.java @@ -0,0 +1,187 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; +import org.opensearch.dataprepper.model.plugin.InvalidPluginDefinitionException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExtensionLoaderTest { + @Mock + private ExtensionClassProvider extensionClassProvider; + @Mock + private PluginCreator pluginCreator; + + private ExtensionLoader createObjectUnderTest() { + return new ExtensionLoader(extensionClassProvider, pluginCreator); + } + + @Test + void loadExtensions_returns_empty_list_when_no_plugin_classes() { + when(extensionClassProvider.loadExtensionPluginClasses()).thenReturn(Collections.emptyList()); + + final List extensionPlugins = createObjectUnderTest().loadExtensions(); + + assertThat(extensionPlugins, notNullValue()); + assertThat(extensionPlugins.size(), equalTo(0)); + } + + @Test + void loadExtensions_returns_single_extension_for_single_plugin_class() { + final Class pluginClass = (Class) mock(ExtensionPlugin.class).getClass(); + + when(extensionClassProvider.loadExtensionPluginClasses()).thenReturn(Collections.singleton(pluginClass)); + + final ExtensionPlugin expectedPlugin = mock(ExtensionPlugin.class); + when(pluginCreator.newPluginInstance( + eq(pluginClass), + any(PluginArgumentsContext.class), + startsWith("extension_plugin"))) + .thenReturn(expectedPlugin); + + final List extensionPlugins = createObjectUnderTest().loadExtensions(); + + assertThat(extensionPlugins, notNullValue()); + assertThat(extensionPlugins.size(), equalTo(1)); + assertThat(extensionPlugins.get(0), equalTo(expectedPlugin)); + } + + @Test + void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes() { + final Collection> pluginClasses = new HashSet<>(); + final Collection expectedPlugins = new ArrayList<>(); + + pluginClasses.add(TestExtension1.class); + pluginClasses.add(TestExtension2.class); + pluginClasses.add(TestExtension3.class); + + for (Class pluginClass : pluginClasses) { + final String expectedPluginName = ExtensionLoader.classNameToPluginName(pluginClass.getSimpleName()); + final ExtensionPlugin extensionPlugin = mock((Class)pluginClass); + + when(pluginCreator.newPluginInstance( + eq(pluginClass), + any(PluginArgumentsContext.class), + eq(expectedPluginName))) + .thenReturn(extensionPlugin); + + pluginClasses.add(pluginClass); + expectedPlugins.add(extensionPlugin); + } + + when(extensionClassProvider.loadExtensionPluginClasses()).thenReturn(pluginClasses); + + final List actualPlugins = (List) createObjectUnderTest().loadExtensions(); + + assertThat(actualPlugins, notNullValue()); + assertThat(actualPlugins.size(), equalTo(pluginClasses.size())); + assertThat(actualPlugins.size(), equalTo(expectedPlugins.size())); + for (ExtensionPlugin expectedPlugin : actualPlugins) { + assertThat(actualPlugins, hasItem(expectedPlugin)); + } + } + + @Test + void loadExtensions_invokes_newPluginInstance_with_PluginArgumentsContext_not_supporting_any_arguments() { + final Class pluginClass = (Class) mock(ExtensionPlugin.class).getClass(); + + when(extensionClassProvider.loadExtensionPluginClasses()).thenReturn(Collections.singleton(pluginClass)); + + when(pluginCreator.newPluginInstance( + any(Class.class), + any(PluginArgumentsContext.class), + anyString())) + .thenReturn(mock(ExtensionPlugin.class)); + + createObjectUnderTest().loadExtensions(); + + final ArgumentCaptor contextArgumentCaptor = + ArgumentCaptor.forClass(PluginArgumentsContext.class); + verify(pluginCreator).newPluginInstance( + eq(pluginClass), + contextArgumentCaptor.capture(), + anyString()); + + final PluginArgumentsContext actualPluginArgumentsContext = contextArgumentCaptor.getValue(); + + final Class[] inputClasses = {String.class}; + assertThrows(InvalidPluginDefinitionException.class, () -> actualPluginArgumentsContext.createArguments(inputClasses)); + } + + @Test + void loadExtensions_invokes_newPluginInstance_with_PluginArgumentsContext_which_returns_empty_arguments_for_empty_classes() { + final Class pluginClass = (Class) mock(ExtensionPlugin.class).getClass(); + + when(extensionClassProvider.loadExtensionPluginClasses()).thenReturn(Collections.singleton(pluginClass)); + + when(pluginCreator.newPluginInstance( + any(Class.class), + any(PluginArgumentsContext.class), + anyString())) + .thenReturn(mock(ExtensionPlugin.class)); + + createObjectUnderTest().loadExtensions(); + + final ArgumentCaptor contextArgumentCaptor = + ArgumentCaptor.forClass(PluginArgumentsContext.class); + verify(pluginCreator).newPluginInstance( + eq(pluginClass), + contextArgumentCaptor.capture(), + anyString()); + + final PluginArgumentsContext actualPluginArgumentsContext = contextArgumentCaptor.getValue(); + + final Object[] arguments = actualPluginArgumentsContext.createArguments(new Class[]{}); + assertThat(arguments, notNullValue()); + assertThat(arguments.length, equalTo(0)); + } + + @ParameterizedTest + @CsvSource({ + "p,p", + "plugin,plugin", + "Plugin,plugin", + "CustomPlugin,custom_plugin", + "MyCustomPlugin,my_custom_plugin", + "MyCustomPlugin$InnerClass,my_custom_plugin_inner_class" + }) + void classNameToPluginName_returns_name_split_by_uppercase(final String input, final String expected) { + assertThat(ExtensionLoader.classNameToPluginName(input), equalTo(expected)); + } + + private interface TestExtension1 extends ExtensionPlugin { + } + private interface TestExtension2 extends ExtensionPlugin { + } + private interface TestExtension3 extends ExtensionPlugin { + } +} \ No newline at end of file diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsApplierTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsApplierTest.java new file mode 100644 index 0000000000..06c9ff2809 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsApplierTest.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExtensionsApplierTest { + @Mock + private DataPrepperExtensionPoints dataPrepperExtensionPoints; + @Mock + private ExtensionLoader extensionLoader; + + private ExtensionsApplier createObjectUnderTest() { + return new ExtensionsApplier( + dataPrepperExtensionPoints, extensionLoader); + } + + @AfterEach + void extensionPointsHasNoInteractions() { + verifyNoInteractions(dataPrepperExtensionPoints); + } + + @Test + void applyExtensions_calls_apply_on_all_loaded_extensions() { + final List extensionPlugins = List.of(mock(ExtensionPlugin.class), mock(ExtensionPlugin.class), mock(ExtensionPlugin.class)); + when(extensionLoader.loadExtensions()).thenReturn((List) extensionPlugins); + + createObjectUnderTest().applyExtensions(); + + for (ExtensionPlugin extensionPlugin : extensionPlugins) { + verify(extensionPlugin).apply(dataPrepperExtensionPoints); + } + } + + @Test + void applyExtensions_with_empty_extensions_is_ok() { + when(extensionLoader.loadExtensions()).thenReturn(Collections.emptyList()); + + createObjectUnderTest().applyExtensions(); + } +} \ No newline at end of file diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsIT.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsIT.java new file mode 100644 index 0000000000..2f0675ff25 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/ExtensionsIT.java @@ -0,0 +1,132 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugin; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensearch.dataprepper.acknowledgements.DefaultAcknowledgementSetManager; +import org.opensearch.dataprepper.event.DefaultEventFactory; +import org.opensearch.dataprepper.model.configuration.PluginSetting; +import org.opensearch.dataprepper.model.plugin.PluginFactory; +import org.opensearch.dataprepper.plugins.test.TestExtension; +import org.opensearch.dataprepper.plugins.TestPluginUsingExtension; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; + +public class ExtensionsIT { + private AnnotationConfigApplicationContext publicContext; + private AnnotationConfigApplicationContext coreContext; + private PluginFactory pluginFactory; + private String pluginName; + private String pipelineName; + + @BeforeEach + void setUp() { + pluginName = "test_plugin_using_extension"; + pipelineName = UUID.randomUUID().toString(); + publicContext = new AnnotationConfigApplicationContext(); + publicContext.refresh(); + + coreContext = new AnnotationConfigApplicationContext(); + coreContext.setParent(publicContext); + + coreContext.scan(DefaultEventFactory.class.getPackage().getName()); + coreContext.scan(DefaultAcknowledgementSetManager.class.getPackage().getName()); + + coreContext.scan(DefaultPluginFactory.class.getPackage().getName()); + coreContext.refresh(); + + pluginFactory = coreContext.getBean(DefaultPluginFactory.class); + } + + @AfterEach + void tearDown() { + TestExtension.reset(); + } + + @Test + void applyExtensions_creates_a_single_instance_of_the_extension() { + assertThat(TestExtension.getConstructedInstances(), equalTo(1)); + } + + @Test + void creating_a_plugin_using_an_extension() { + final String requiredStringValue = UUID.randomUUID().toString(); + final String optionalStringValue = UUID.randomUUID().toString(); + final Map pluginSettingMap = new HashMap<>(); + pluginSettingMap.put("required_string", requiredStringValue); + pluginSettingMap.put("optional_string", optionalStringValue); + final PluginSetting pluginSetting = createPluginSettings(pluginSettingMap); + + final TestPluggableInterface pluggableInterface = pluginFactory.loadPlugin(TestPluggableInterface.class, pluginSetting); + + assertThat(pluggableInterface, notNullValue()); + assertThat(pluggableInterface, instanceOf(TestPluginUsingExtension.class)); + final TestPluginUsingExtension testPluginUsingExtension = (TestPluginUsingExtension) pluggableInterface; + assertThat(testPluginUsingExtension.getExtensionModel(), notNullValue()); + assertThat(testPluginUsingExtension.getExtensionModel().getExtensionId(), notNullValue()); + } + + @Test + void creating_multiple_plugins_using_an_extension() { + final String requiredStringValue = UUID.randomUUID().toString(); + final String optionalStringValue = UUID.randomUUID().toString(); + final Map pluginSettingMap = new HashMap<>(); + pluginSettingMap.put("required_string", requiredStringValue); + pluginSettingMap.put("optional_string", optionalStringValue); + final PluginSetting pluginSetting = createPluginSettings(pluginSettingMap); + + final Set extensionIds = new HashSet<>(); + final List models = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + final TestPluggableInterface pluggableInterface = pluginFactory.loadPlugin(TestPluggableInterface.class, pluginSetting); + + assertThat(pluggableInterface, notNullValue()); + assertThat(pluggableInterface, instanceOf(TestPluginUsingExtension.class)); + final TestPluginUsingExtension testPluginUsingExtension = (TestPluginUsingExtension) pluggableInterface; + assertThat(testPluginUsingExtension.getExtensionModel(), notNullValue()); + assertThat(testPluginUsingExtension.getExtensionModel().getExtensionId(), notNullValue()); + + extensionIds.add(testPluginUsingExtension.getExtensionModel().getExtensionId()); + + models.add(testPluginUsingExtension.getExtensionModel()); + } + + assertThat(extensionIds.size(), equalTo(1)); + + assertThat(models.size(), equalTo(5)); + for (int i = 0; i < models.size(); i++) { + for (int j = 0; j < models.size(); j++) { + if (i != j) { + final TestExtension.TestModel model = models.get(i); + final TestExtension.TestModel otherModel = models.get(j); + assertThat(model, not(sameInstance(otherModel))); + } + } + } + } + + private PluginSetting createPluginSettings(final Map pluginSettingMap) { + final PluginSetting pluginSetting = new PluginSetting(pluginName, pluginSettingMap); + pluginSetting.setPipelineName(pipelineName); + return pluginSetting; + } +} diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProviderTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProviderTest.java index 496e9ccfa0..8d1ee50853 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProviderTest.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginBeanFactoryProviderTest.java @@ -5,13 +5,17 @@ package org.opensearch.dataprepper.plugin; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -21,34 +25,45 @@ class PluginBeanFactoryProviderTest { + private ApplicationContext context; + + @BeforeEach + void setUp() { + context = mock(ApplicationContext.class); + } + + private PluginBeanFactoryProvider createObjectUnderTest() { + return new PluginBeanFactoryProvider(context); + } + @Test void testPluginBeanFactoryProviderUsesParentContext() { - final ApplicationContext context = mock(ApplicationContext.class); + doReturn(context).when(context).getParent(); - new PluginBeanFactoryProvider(context); + createObjectUnderTest(); verify(context).getParent(); } @Test void testPluginBeanFactoryProviderRequiresContext() { - assertThrows(NullPointerException.class, () -> new PluginBeanFactoryProvider(null)); + context = null; + assertThrows(NullPointerException.class, () -> createObjectUnderTest()); } @Test void testPluginBeanFactoryProviderRequiresParentContext() { - final ApplicationContext context = mock(ApplicationContext.class); + context = mock(ApplicationContext.class); - assertThrows(NullPointerException.class, () -> new PluginBeanFactoryProvider(context)); + assertThrows(NullPointerException.class, () -> createObjectUnderTest()); } @Test void testPluginBeanFactoryProviderGetReturnsBeanFactory() { - final ApplicationContext context = mock(ApplicationContext.class); doReturn(context).when(context).getParent(); - final PluginBeanFactoryProvider beanFactoryProvider = new PluginBeanFactoryProvider(context); + final PluginBeanFactoryProvider beanFactoryProvider = createObjectUnderTest(); verify(context).getParent(); assertThat(beanFactoryProvider.get(), is(instanceOf(BeanFactory.class))); @@ -56,10 +71,9 @@ void testPluginBeanFactoryProviderGetReturnsBeanFactory() { @Test void testPluginBeanFactoryProviderGetReturnsUniqueBeanFactory() { - final ApplicationContext context = mock(ApplicationContext.class); doReturn(context).when(context).getParent(); - final PluginBeanFactoryProvider beanFactoryProvider = new PluginBeanFactoryProvider(context); + final PluginBeanFactoryProvider beanFactoryProvider = createObjectUnderTest(); final BeanFactory isolatedBeanFactoryA = beanFactoryProvider.get(); final BeanFactory isolatedBeanFactoryB = beanFactoryProvider.get(); @@ -67,4 +81,19 @@ void testPluginBeanFactoryProviderGetReturnsUniqueBeanFactory() { assertThat(isolatedBeanFactoryA, not(sameInstance(isolatedBeanFactoryB))); } + @Test + void getSharedPluginApplicationContext_returns_created_ApplicationContext() { + doReturn(context).when(context).getParent(); + final GenericApplicationContext actualContext = createObjectUnderTest().getSharedPluginApplicationContext(); + + assertThat(actualContext, notNullValue()); + assertThat(actualContext.getParent(), equalTo(context)); + } + + @Test + void getSharedPluginApplicationContext_called_multiple_times_returns_same_instance() { + doReturn(context).when(context).getParent(); + final PluginBeanFactoryProvider objectUnderTest = createObjectUnderTest(); + assertThat(objectUnderTest.getSharedPluginApplicationContext(), sameInstance(objectUnderTest.getSharedPluginApplicationContext())); + } } \ No newline at end of file diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginCreatorTest.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginCreatorTest.java index a8a84ecebd..b43dd40ea0 100644 --- a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginCreatorTest.java +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugin/PluginCreatorTest.java @@ -27,7 +27,7 @@ class PluginCreatorTest { private PluginSetting pluginSetting; private String pluginName; - private PluginArgumentsContext pluginConstructionContext; + private ComponentPluginArgumentsContext pluginConstructionContext; public static class ValidPluginClass { private final PluginSetting pluginSetting; @@ -86,7 +86,7 @@ void setUp() { pluginName = UUID.randomUUID().toString(); - pluginConstructionContext = mock(PluginArgumentsContext.class); + pluginConstructionContext = mock(ComponentPluginArgumentsContext.class); } private PluginCreator createObjectUnderTest() { diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestPluginUsingExtension.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestPluginUsingExtension.java new file mode 100644 index 0000000000..c29f2b0587 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/TestPluginUsingExtension.java @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugins; + +import org.opensearch.dataprepper.model.annotations.DataPrepperPlugin; +import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor; +import org.opensearch.dataprepper.plugin.TestPluggableInterface; +import org.opensearch.dataprepper.plugin.TestPluginConfiguration; +import org.opensearch.dataprepper.plugins.test.TestExtension; + +/** + * Used for integration testing the plugin framework with extensions. + */ +@DataPrepperPlugin(name = "test_plugin_using_extension", pluginType = TestPluggableInterface.class, pluginConfigurationType = TestPluginConfiguration.class) +public class TestPluginUsingExtension implements TestPluggableInterface { + private final TestPluginConfiguration configuration; + private final TestExtension.TestModel extensionModel; + + @DataPrepperPluginConstructor + public TestPluginUsingExtension(final TestPluginConfiguration configuration, + final TestExtension.TestModel extensionModel) { + this.configuration = configuration; + this.extensionModel = extensionModel; + } + + public TestPluginConfiguration getConfiguration() { + return configuration; + } + + public TestExtension.TestModel getExtensionModel() { + return extensionModel; + } +} diff --git a/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/test/TestExtension.java b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/test/TestExtension.java new file mode 100644 index 0000000000..bd0349f924 --- /dev/null +++ b/data-prepper-core/src/test/java/org/opensearch/dataprepper/plugins/test/TestExtension.java @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.dataprepper.plugins.test; + +import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor; +import org.opensearch.dataprepper.model.plugin.ExtensionPlugin; +import org.opensearch.dataprepper.model.plugin.ExtensionPoints; +import org.opensearch.dataprepper.model.plugin.ExtensionProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public class TestExtension implements ExtensionPlugin { + private static final Logger LOG = LoggerFactory.getLogger(TestExtension.class); + private static final AtomicInteger CONSTRUCTED_COUNT = new AtomicInteger(0); + private final String extensionId; + + @DataPrepperPluginConstructor + public TestExtension() { + LOG.info("Constructing test extension plugin."); + CONSTRUCTED_COUNT.incrementAndGet(); + extensionId = UUID.randomUUID().toString(); + } + + @Override + public void apply(final ExtensionPoints extensionPoints) { + LOG.info("Applying test extension."); + extensionPoints.addExtensionProvider(new TestExtensionProvider()); + } + + public static void reset() { + CONSTRUCTED_COUNT.set(0); + } + + public static int getConstructedInstances() { + return CONSTRUCTED_COUNT.get(); + } + + public static class TestModel { + private final String extensionId; + + private TestModel(final String extensionId) { + + this.extensionId = extensionId; + } + public String getExtensionId() { + return this.extensionId; + } + } + + private class TestExtensionProvider implements ExtensionProvider { + + @Override + public Optional provideInstance(final Context context) { + return Optional.of(new TestModel(extensionId)); + } + + @Override + public Class supportedClass() { + return TestModel.class; + } + } +}