Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.model.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ExtensionDependsOn {
/**
* The list of classes that this extension depends on
* @return Array of Class objects representing the classes this extension depends on
*/
Class<?>[] dependentClasses() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.model.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to specify that a class provides an extension and its dependencies.
* This annotation can be used to declare what extension points a class provides
* and what other extension points it depends on.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ExtensionProvides {
/**
* The list of classes that this extension provides
* @return Array of Class objects representing the classes this extension provides
*/
Class<?>[] providedClasses() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public interface ExtensionPoints {
* @since 2.3
*/
void addExtensionProvider(ExtensionProvider<?> extensionProvider);

<T> T getExtensionProvider(Class<T> type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public void addExtensionProvider(final ExtensionProvider extensionProvider) {
providerClassesSet.add(extensionProvider.supportedClass());
}

@Override
public <T> T getExtensionProvider(final Class<T> type) {
sharedApplicationContext.refresh();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We found that multiple calls to refresh() will fail. So this solution only works with one dependency. This is fine for now, but we need to remember to fix this.

return sharedApplicationContext.getBean(type);
}

private static class EmptyContext implements ExtensionProvider.Context {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package org.opensearch.dataprepper.plugin;

import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin;
import org.opensearch.dataprepper.model.annotations.ExtensionDependsOn;
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel;
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException;
Expand All @@ -27,10 +29,17 @@ public class ExtensionLoader {
public class ExtensionPluginWithContext {
ExtensionPlugin extensionPlugin;
boolean configured;
Class<?>[] dependentClasses;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be final (along with the two above).

Class<?>[] providedClasses;

public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin, final boolean isConfigured) {
public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin,
final boolean isConfigured,
final Class<?>[] dependentClasses,
final Class<?>[] providedClasses) {
this.extensionPlugin = extensionPlugin;
this.configured = isConfigured;
this.dependentClasses = dependentClasses;
this.providedClasses = providedClasses;
}

public ExtensionPlugin getExtensionPlugin() {
Expand All @@ -40,6 +49,14 @@ public ExtensionPlugin getExtensionPlugin() {
public boolean isConfigured() {
return configured;
}

public Class<?>[] getDependentClasses() {
return dependentClasses;
}

public Class<?>[] getProvidedClasses() {
return providedClasses;
}
}

private Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator;
Expand Down Expand Up @@ -73,9 +90,17 @@ public List<? extends ExtensionPlugin> loadExtensions() {
final String pluginName = convertClassToName(extensionClass);
try {
final PluginArgumentsContext pluginArgumentsContext = getConstructionContext(extensionClass);
final ExtensionProvides extensionProvidesAnnotation = extensionClass.getAnnotation(ExtensionProvides.class);
final ExtensionDependsOn extensionDependsOnAnnotation = extensionClass.getAnnotation(ExtensionDependsOn.class);

final Class<?>[] providedClasses = extensionProvidesAnnotation != null ?
extensionProvidesAnnotation.providedClasses() : new Class<?>[]{};
final Class<?>[] dependentClasses = extensionDependsOnAnnotation != null ?
extensionDependsOnAnnotation.dependentClasses() : new Class<?>[]{};

final Object config = pluginArgumentsContext.getArgument(0);
return new ExtensionPluginWithContext(extensionPluginCreator.newPluginInstance(
extensionClass, pluginArgumentsContext, pluginName), (config != null));
extensionClass, pluginArgumentsContext, pluginName), (config != null), dependentClasses, providedClasses);
} catch (Exception e) {
final PluginError pluginError = PluginError.builder()
.componentType(PipelinesDataFlowModel.EXTENSION_PLUGIN_TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import javax.inject.Named;

import java.util.Arrays;
import java.util.Comparator;

@Named
Expand All @@ -21,6 +22,35 @@ public PluginCreator pluginCreator(

@Bean(name = "extensionsLoaderComparator")
public Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator() {
return Comparator.comparing(ExtensionLoader.ExtensionPluginWithContext::isConfigured).reversed();
return (extensionOne, extensionTwo) -> {
// First, compare by configuration status (configured ones first)
int configCompare = Boolean.compare(extensionTwo.isConfigured(), extensionOne.isConfigured());
if (configCompare != 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not the final sorting. It may be the case a configured depends on a non-configured. In which case the dependency will not exist. But, we can handle this later.

return configCompare;
}

// Get the provided and dependent classes for both extensions
Class<?>[] extensionOneProvidedClasses = extensionOne.getProvidedClasses();
Class<?>[] extensionTwoProvidedClasses = extensionTwo.getProvidedClasses();
Class<?>[] extensionOneDependentClasses = extensionOne.getDependentClasses();
Class<?>[] extensionTwoDependentClasses = extensionTwo.getDependentClasses();

// If extensionOne provides any classes that extensionTwo depends on, extensionOne should go first
if (containsAnyExtensionDependencies(extensionOneProvidedClasses, extensionTwoDependentClasses)) {
return -1;
}

// If extensionTwo provides any classes that extensionOne depends on, extensionTwo should go first
if (containsAnyExtensionDependencies(extensionTwoProvidedClasses, extensionOneDependentClasses)) {
return 1;
}

return 0;
};
}

private boolean containsAnyExtensionDependencies(final Class<?>[] provided, final Class<?>[] dependencies) {
return Arrays.stream(dependencies).anyMatch(dep ->
Arrays.asList(provided).contains(dep));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ void addExtensionProvider_should_registerBean_as_prototype() {
verifyRegisterBeanAsPrototype(coreApplicationContext);
}

@Test
void getExtensionProvider_refreshes_shared_context_and_returns_correct_bean() {
final Class<DefaultPluginFactory> defaultPluginFactoryClass = DefaultPluginFactory.class;
final DefaultPluginFactory defaultPluginFactory = mock(DefaultPluginFactory.class);

when(sharedApplicationContext.getBean(defaultPluginFactoryClass)).thenReturn(defaultPluginFactory);

final DefaultPluginFactory result = createObjectUnderTest().getExtensionProvider(defaultPluginFactoryClass);

assertThat(result, equalTo(defaultPluginFactory));

verify(sharedApplicationContext).refresh();
}

private void verifyRegisterBeanWithProvideInstance(final GenericApplicationContext applicationContext) {
reset(extensionProvider);
final ArgumentCaptor<Supplier<Object>> supplierArgumentCaptor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.model.annotations.ExtensionDependsOn;
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel;
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException;
Expand Down Expand Up @@ -45,6 +47,7 @@
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -224,7 +227,7 @@ void loadExtensions_throws_InvalidPluginConfigurationException_when_extensionPlu
}

@Test
void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes() {
void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes_in_correct_order() {
final Collection<Class<? extends ExtensionPlugin>> pluginClasses = new HashSet<>();
final Collection<ExtensionPlugin> expectedPlugins = new ArrayList<>();

Expand Down Expand Up @@ -256,6 +259,13 @@ void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes() {
for (ExtensionPlugin expectedPlugin : actualPlugins) {
assertThat(actualPlugins, hasItem(expectedPlugin));
}

assertThat(actualPlugins.get(0), instanceOf(TestExtension2.class));
assertTrue(actualPlugins.get(1) instanceof TestExtension1 || actualPlugins.get(1) instanceof TestExtension3,
"Expected result to be either TestExtension1 or TestExtension3 but was " + actualPlugins.get(1).getClass().getName());

assertTrue(actualPlugins.get(2) instanceof TestExtension1 || actualPlugins.get(2) instanceof TestExtension3,
"Expected result to be either TestExtension1 or TestExtension3 but was " + actualPlugins.get(2).getClass().getName());
assertThat(pluginErrorCollector.getPluginErrors().isEmpty(), is(true));
}

Expand Down Expand Up @@ -335,10 +345,15 @@ private static Stream<Arguments> validExtensionConfigs() {
null);
}

@ExtensionDependsOn(dependentClasses = TestExtensionConfig.class)
private interface TestExtension1 extends ExtensionPlugin {
}

@ExtensionProvides(providedClasses = TestExtensionConfig.class)
private interface TestExtension2 extends ExtensionPlugin {
}

@ExtensionDependsOn(dependentClasses = TestExtensionConfig.class)
private interface TestExtension3 extends ExtensionPlugin {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,29 @@ public void test_pluginCreator() {

@Test
public void test_extensionsLoaderComparator() {
final Class<?>[] classes = {DefaultPluginFactory.class};

ExtensionLoader.ExtensionPluginWithContext context1 = mock(ExtensionLoader.ExtensionPluginWithContext.class);
ExtensionLoader.ExtensionPluginWithContext context2 = mock(ExtensionLoader.ExtensionPluginWithContext.class);
Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator = pluginCreatorContext.extensionsLoaderComparator();
assertNotNull(extensionsLoaderComparator);
when(context1.isConfigured()).thenReturn(true);
when(context1.getDependentClasses()).thenReturn(classes);
when(context1.getProvidedClasses()).thenReturn(new Class<?>[]{});

when(context2.isConfigured()).thenReturn(true);
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(0));
when(context2.getProvidedClasses()).thenReturn(classes);
when(context2.getDependentClasses()).thenReturn(new Class<?>[]{});
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(1));

when(context1.isConfigured()).thenReturn(false);
when(context1.getDependentClasses()).thenReturn(new Class<?>[]{});
when(context1.getProvidedClasses()).thenReturn(classes);

when(context2.isConfigured()).thenReturn(false);
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(0));
when(context2.getProvidedClasses()).thenReturn(new Class<?>[]{});
when(context2.getDependentClasses()).thenReturn(classes);
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(-1));
when(context1.isConfigured()).thenReturn(false);
when(context2.isConfigured()).thenReturn(true);
assertThat(extensionsLoaderComparator.compare(context1, context2), greaterThan(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

package org.opensearch.dataprepper.plugins.aws;

import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier;
import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin;
import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor;
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
import org.opensearch.dataprepper.model.plugin.ExtensionPoints;

Expand All @@ -15,6 +17,7 @@
* Data Prepper as an extension plugin. Everything starts from here.
*/
@DataPrepperExtensionPlugin(modelType = AwsPluginConfig.class, rootKeyJsonPath = "/aws/configurations")
@ExtensionProvides(providedClasses = {AwsCredentialsSupplier.class})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? AwsCredentialsSupplier is not an extension class, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AwsCredentialsSupplier is not an extension class. It is provided by an extension class. This is declaring that this particular extension provides it.

public class AwsPlugin implements ExtensionPlugin {
private final DefaultAwsCredentialsSupplier defaultAwsCredentialsSupplier;

Expand Down
Loading
Loading