Skip to content

Commit cc7534b

Browse files
committed
Add support for extensions to depend upon other extensions
Signed-off-by: Taylor Gray <tylgry@amazon.com>
1 parent fab4ac1 commit cc7534b

17 files changed

Lines changed: 269 additions & 189 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.dataprepper.model.annotations;
7+
8+
import java.lang.annotation.Documented;
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
@Documented
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Target({ElementType.TYPE})
17+
public @interface ExtensionDependsOn {
18+
/**
19+
* The list of classes that this extension depends on
20+
* @return Array of Class objects representing the classes this extension depends on
21+
*/
22+
Class<?>[] dependentClasses() default {};
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.dataprepper.model.annotations;
7+
8+
import java.lang.annotation.Documented;
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
/**
15+
* Annotation to specify that a class provides an extension and its dependencies.
16+
* This annotation can be used to declare what extension points a class provides
17+
* and what other extension points it depends on.
18+
*/
19+
@Documented
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@Target({ElementType.TYPE})
22+
public @interface ExtensionProvides {
23+
/**
24+
* The list of classes that this extension provides
25+
* @return Array of Class objects representing the classes this extension provides
26+
*/
27+
Class<?>[] providedClasses() default {};
28+
}

data-prepper-api/src/main/java/org/opensearch/dataprepper/model/plugin/ExtensionPoints.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,6 @@ public interface ExtensionPoints {
2020
* @since 2.3
2121
*/
2222
void addExtensionProvider(ExtensionProvider<?> extensionProvider);
23+
24+
<T> T getExtensionProvider(Class<T> type);
2325
}

data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPoints.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ public void addExtensionProvider(final ExtensionProvider extensionProvider) {
5050
providerClassesSet.add(extensionProvider.supportedClass());
5151
}
5252

53+
@Override
54+
public <T> T getExtensionProvider(final Class<T> type) {
55+
sharedApplicationContext.refresh();
56+
return sharedApplicationContext.getBean(type);
57+
}
58+
5359
private static class EmptyContext implements ExtensionProvider.Context {
5460

5561
}

data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/ExtensionLoader.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package org.opensearch.dataprepper.plugin;
77

88
import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin;
9+
import org.opensearch.dataprepper.model.annotations.ExtensionDependsOn;
10+
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
911
import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel;
1012
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
1113
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException;
@@ -27,10 +29,17 @@ public class ExtensionLoader {
2729
public class ExtensionPluginWithContext {
2830
ExtensionPlugin extensionPlugin;
2931
boolean configured;
32+
Class<?>[] dependentClasses;
33+
Class<?>[] providedClasses;
3034

31-
public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin, final boolean isConfigured) {
35+
public ExtensionPluginWithContext(final ExtensionPlugin extensionPlugin,
36+
final boolean isConfigured,
37+
final Class<?>[] dependentClasses,
38+
final Class<?>[] providedClasses) {
3239
this.extensionPlugin = extensionPlugin;
3340
this.configured = isConfigured;
41+
this.dependentClasses = dependentClasses;
42+
this.providedClasses = providedClasses;
3443
}
3544

3645
public ExtensionPlugin getExtensionPlugin() {
@@ -40,6 +49,14 @@ public ExtensionPlugin getExtensionPlugin() {
4049
public boolean isConfigured() {
4150
return configured;
4251
}
52+
53+
public Class<?>[] getDependentClasses() {
54+
return dependentClasses;
55+
}
56+
57+
public Class<?>[] getProvidedClasses() {
58+
return providedClasses;
59+
}
4360
}
4461

4562
private Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator;
@@ -73,9 +90,17 @@ public List<? extends ExtensionPlugin> loadExtensions() {
7390
final String pluginName = convertClassToName(extensionClass);
7491
try {
7592
final PluginArgumentsContext pluginArgumentsContext = getConstructionContext(extensionClass);
93+
final ExtensionProvides extensionProvidesAnnotation = extensionClass.getAnnotation(ExtensionProvides.class);
94+
final ExtensionDependsOn extensionDependsOnAnnotation = extensionClass.getAnnotation(ExtensionDependsOn.class);
95+
96+
final Class<?>[] providedClasses = extensionProvidesAnnotation != null ?
97+
extensionProvidesAnnotation.providedClasses() : new Class<?>[]{};
98+
final Class<?>[] dependentClasses = extensionDependsOnAnnotation != null ?
99+
extensionDependsOnAnnotation.dependentClasses() : new Class<?>[]{};
100+
76101
final Object config = pluginArgumentsContext.getArgument(0);
77102
return new ExtensionPluginWithContext(extensionPluginCreator.newPluginInstance(
78-
extensionClass, pluginArgumentsContext, pluginName), (config != null));
103+
extensionClass, pluginArgumentsContext, pluginName), (config != null), dependentClasses, providedClasses);
79104
} catch (Exception e) {
80105
final PluginError pluginError = PluginError.builder()
81106
.componentType(PipelinesDataFlowModel.EXTENSION_PLUGIN_TYPE)

data-prepper-plugin-framework/src/main/java/org/opensearch/dataprepper/plugin/PluginCreatorContext.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import javax.inject.Named;
66

7+
import java.util.Arrays;
78
import java.util.Comparator;
89

910
@Named
@@ -21,6 +22,37 @@ public PluginCreator pluginCreator(
2122

2223
@Bean(name = "extensionsLoaderComparator")
2324
public Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator() {
24-
return Comparator.comparing(ExtensionLoader.ExtensionPluginWithContext::isConfigured).reversed();
25+
return (extensionOne, extensionTwo) -> {
26+
// First, compare by configuration status (configured ones first)
27+
int configCompare = Boolean.compare(extensionTwo.isConfigured(), extensionOne.isConfigured());
28+
if (configCompare != 0) {
29+
return configCompare;
30+
}
31+
32+
// Get the provided and dependent classes for both extensions
33+
Class<?>[] extensionOneProvidedClasses = extensionOne.getProvidedClasses();
34+
Class<?>[] extensionTwoProvidedClasses = extensionTwo.getProvidedClasses();
35+
Class<?>[] extensionOneDependentClasses = extensionOne.getDependentClasses();
36+
Class<?>[] extensionTwoDependentClasses = extensionTwo.getDependentClasses();
37+
38+
// If extensionOne provides any classes that extensionTwo depends on, extensionOne should go first
39+
if (containsAnyExtensionDependencies(extensionOneProvidedClasses, extensionTwoDependentClasses)) {
40+
return -1;
41+
}
42+
43+
// If extensionTwo provides any classes that extensionOne depends on, extensionTwo should go first
44+
if (containsAnyExtensionDependencies(extensionTwoProvidedClasses, extensionOneDependentClasses)) {
45+
return 1;
46+
}
47+
48+
// If neither extension depends on the other, compare by number of dependencies
49+
// Extensions with fewer dependencies will go first
50+
return Integer.compare(extensionOneDependentClasses.length, extensionTwoDependentClasses.length);
51+
};
52+
}
53+
54+
private boolean containsAnyExtensionDependencies(final Class<?>[] provided, final Class<?>[] dependencies) {
55+
return Arrays.stream(dependencies).anyMatch(dep ->
56+
Arrays.asList(provided).contains(dep));
2557
}
2658
}

data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/DataPrepperExtensionPointsTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ void addExtensionProvider_should_registerBean_as_prototype() {
127127
verifyRegisterBeanAsPrototype(coreApplicationContext);
128128
}
129129

130+
@Test
131+
void getExtensionProvider_refreshes_shared_context_and_returns_correct_bean() {
132+
final Class<DefaultPluginFactory> defaultPluginFactoryClass = DefaultPluginFactory.class;
133+
final DefaultPluginFactory defaultPluginFactory = mock(DefaultPluginFactory.class);
134+
135+
when(sharedApplicationContext.getBean(defaultPluginFactoryClass)).thenReturn(defaultPluginFactory);
136+
137+
final DefaultPluginFactory result = createObjectUnderTest().getExtensionProvider(defaultPluginFactoryClass);
138+
139+
assertThat(result, equalTo(defaultPluginFactory));
140+
141+
verify(sharedApplicationContext).refresh();
142+
}
143+
130144
private void verifyRegisterBeanWithProvideInstance(final GenericApplicationContext applicationContext) {
131145
reset(extensionProvider);
132146
final ArgumentCaptor<Supplier<Object>> supplierArgumentCaptor =

data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/ExtensionLoaderTest.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import org.mockito.Captor;
1919
import org.mockito.Mock;
2020
import org.mockito.junit.jupiter.MockitoExtension;
21+
import org.opensearch.dataprepper.model.annotations.ExtensionDependsOn;
22+
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
2123
import org.opensearch.dataprepper.model.configuration.PipelinesDataFlowModel;
2224
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
2325
import org.opensearch.dataprepper.model.plugin.InvalidPluginConfigurationException;
@@ -45,6 +47,7 @@
4547
import static org.hamcrest.CoreMatchers.nullValue;
4648
import static org.hamcrest.MatcherAssert.assertThat;
4749
import static org.junit.jupiter.api.Assertions.assertThrows;
50+
import static org.junit.jupiter.api.Assertions.assertTrue;
4851
import static org.mockito.ArgumentMatchers.any;
4952
import static org.mockito.ArgumentMatchers.anyString;
5053
import static org.mockito.ArgumentMatchers.eq;
@@ -224,7 +227,7 @@ void loadExtensions_throws_InvalidPluginConfigurationException_when_extensionPlu
224227
}
225228

226229
@Test
227-
void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes() {
230+
void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes_in_correct_order() {
228231
final Collection<Class<? extends ExtensionPlugin>> pluginClasses = new HashSet<>();
229232
final Collection<ExtensionPlugin> expectedPlugins = new ArrayList<>();
230233

@@ -256,6 +259,13 @@ void loadExtensions_returns_multiple_extensions_for_multiple_plugin_classes() {
256259
for (ExtensionPlugin expectedPlugin : actualPlugins) {
257260
assertThat(actualPlugins, hasItem(expectedPlugin));
258261
}
262+
263+
assertThat(actualPlugins.get(0), instanceOf(TestExtension2.class));
264+
assertTrue(actualPlugins.get(1) instanceof TestExtension1 || actualPlugins.get(1) instanceof TestExtension3,
265+
"Expected result to be either TestExtension1 or TestExtension3 but was " + actualPlugins.get(1).getClass().getName());
266+
267+
assertTrue(actualPlugins.get(2) instanceof TestExtension1 || actualPlugins.get(2) instanceof TestExtension3,
268+
"Expected result to be either TestExtension1 or TestExtension3 but was " + actualPlugins.get(2).getClass().getName());
259269
assertThat(pluginErrorCollector.getPluginErrors().isEmpty(), is(true));
260270
}
261271

@@ -335,10 +345,15 @@ private static Stream<Arguments> validExtensionConfigs() {
335345
null);
336346
}
337347

348+
@ExtensionDependsOn(dependentClasses = TestExtensionConfig.class)
338349
private interface TestExtension1 extends ExtensionPlugin {
339350
}
351+
352+
@ExtensionProvides(providedClasses = TestExtensionConfig.class)
340353
private interface TestExtension2 extends ExtensionPlugin {
341354
}
355+
356+
@ExtensionDependsOn(dependentClasses = TestExtensionConfig.class)
342357
private interface TestExtension3 extends ExtensionPlugin {
343358
}
344359

data-prepper-plugin-framework/src/test/java/org/opensearch/dataprepper/plugin/PluginCreatorContextTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,29 @@ public void test_pluginCreator() {
4040

4141
@Test
4242
public void test_extensionsLoaderComparator() {
43+
final Class<?>[] classes = {DefaultPluginFactory.class};
44+
4345
ExtensionLoader.ExtensionPluginWithContext context1 = mock(ExtensionLoader.ExtensionPluginWithContext.class);
4446
ExtensionLoader.ExtensionPluginWithContext context2 = mock(ExtensionLoader.ExtensionPluginWithContext.class);
4547
Comparator<ExtensionLoader.ExtensionPluginWithContext> extensionsLoaderComparator = pluginCreatorContext.extensionsLoaderComparator();
4648
assertNotNull(extensionsLoaderComparator);
4749
when(context1.isConfigured()).thenReturn(true);
50+
when(context1.getDependentClasses()).thenReturn(classes);
51+
when(context1.getProvidedClasses()).thenReturn(new Class<?>[]{});
52+
4853
when(context2.isConfigured()).thenReturn(true);
49-
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(0));
54+
when(context2.getProvidedClasses()).thenReturn(classes);
55+
when(context2.getDependentClasses()).thenReturn(new Class<?>[]{});
56+
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(1));
57+
5058
when(context1.isConfigured()).thenReturn(false);
59+
when(context1.getDependentClasses()).thenReturn(new Class<?>[]{});
60+
when(context1.getProvidedClasses()).thenReturn(classes);
61+
5162
when(context2.isConfigured()).thenReturn(false);
52-
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(0));
63+
when(context2.getProvidedClasses()).thenReturn(new Class<?>[]{});
64+
when(context2.getDependentClasses()).thenReturn(classes);
65+
assertThat(extensionsLoaderComparator.compare(context1, context2), equalTo(-1));
5366
when(context1.isConfigured()).thenReturn(false);
5467
when(context2.isConfigured()).thenReturn(true);
5568
assertThat(extensionsLoaderComparator.compare(context1, context2), greaterThan(0));

data-prepper-plugins/aws-plugin/src/main/java/org/opensearch/dataprepper/plugins/aws/AwsPlugin.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
package org.opensearch.dataprepper.plugins.aws;
77

8+
import org.opensearch.dataprepper.aws.api.AwsCredentialsSupplier;
89
import org.opensearch.dataprepper.model.annotations.DataPrepperExtensionPlugin;
910
import org.opensearch.dataprepper.model.annotations.DataPrepperPluginConstructor;
11+
import org.opensearch.dataprepper.model.annotations.ExtensionProvides;
1012
import org.opensearch.dataprepper.model.plugin.ExtensionPlugin;
1113
import org.opensearch.dataprepper.model.plugin.ExtensionPoints;
1214

@@ -15,6 +17,7 @@
1517
* Data Prepper as an extension plugin. Everything starts from here.
1618
*/
1719
@DataPrepperExtensionPlugin(modelType = AwsPluginConfig.class, rootKeyJsonPath = "/aws/configurations")
20+
@ExtensionProvides(providedClasses = {AwsCredentialsSupplier.class})
1821
public class AwsPlugin implements ExtensionPlugin {
1922
private final DefaultAwsCredentialsSupplier defaultAwsCredentialsSupplier;
2023

0 commit comments

Comments
 (0)