diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java
index 05adbbf..d996d3b 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Components.java
@@ -10,6 +10,7 @@
import com.launchdarkly.sdk.server.ComponentsImpl.LoggingConfigurationBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.NullDataSourceFactory;
import com.launchdarkly.sdk.server.ComponentsImpl.PersistentDataStoreBuilderImpl;
+import com.launchdarkly.sdk.server.ComponentsImpl.PluginsConfigurationBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.PollingDataSourceBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.ServiceEndpointsBuilderImpl;
import com.launchdarkly.sdk.server.ComponentsImpl.StreamingDataSourceBuilderImpl;
@@ -21,6 +22,7 @@
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
+import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
@@ -445,6 +447,26 @@ public static HooksConfigurationBuilder hooks() {
return new HooksConfigurationBuilderImpl();
}
+ /**
+ * Returns a builder for configuring plugins.
+ *
+ * Passing this to {@link LDConfig.Builder#plugins(com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder)},
+ * after setting any desired plugins on the builder, applies this configuration to the SDK.
+ *
+ * List plugins = myCreatePluginsFunc();
+ * LDConfig config = new LDConfig.Builder()
+ * .plugins(
+ * Components.plugins()
+ * .setPlugins(plugins)
+ * )
+ * .build();
+ *
+ * @return a {@link PluginsConfigurationBuilder} that can be used for customization
+ */
+ public static PluginsConfigurationBuilder plugins() {
+ return new PluginsConfigurationBuilderImpl();
+ }
+
/**
* Returns a wrapper information builder.
*
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
index ea40f41..cf9351b 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java
@@ -17,6 +17,7 @@
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder;
+import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder;
@@ -35,6 +36,8 @@
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
import com.launchdarkly.sdk.server.subsystems.PersistentDataStore;
+import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
+
import okhttp3.Credentials;
import java.io.IOException;
@@ -478,6 +481,19 @@ public HookConfiguration build() {
}
}
+ static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {
+ public static PluginsConfigurationBuilderImpl fromPluginsConfiguration(PluginsConfiguration pluginsConfiguration) {
+ PluginsConfigurationBuilderImpl builder = new PluginsConfigurationBuilderImpl();
+ builder.setPlugins(pluginsConfiguration.getPlugins());
+ return builder;
+ }
+
+ @Override
+ public PluginsConfiguration build() {
+ return new PluginsConfiguration(plugins);
+ }
+ }
+
static final class WrapperInfoBuilderImpl extends WrapperInfoBuilder {
public WrapperInfoBuilderImpl() {
this(null, null);
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java
index 4e6bda3..9b29053 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java
@@ -9,6 +9,10 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;
import com.launchdarkly.sdk.internal.http.HttpHelpers;
+import com.launchdarkly.sdk.server.integrations.EnvironmentMetadata;
+import com.launchdarkly.sdk.server.integrations.Hook;
+import com.launchdarkly.sdk.server.integrations.Plugin;
+import com.launchdarkly.sdk.server.integrations.SdkMetadata;
import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider;
import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration;
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
@@ -30,6 +34,9 @@
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
@@ -206,13 +213,33 @@ public LDClient(String sdkKey, LDConfig config) {
EvaluatorInterface evaluator = new InputValidatingEvaluator(dataStore, bigSegmentStoreWrapper, eventProcessor, evaluationLogger);
+ // build environment metadata for plugins
+ SdkMetadata sdkMetadata;
+ if (config.wrapperInfo == null) {
+ sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION);
+ } else {
+ sdkMetadata = new SdkMetadata("JavaClient", Version.SDK_VERSION, config.wrapperInfo.getWrapperName(), config.wrapperInfo.getWrapperVersion());
+ }
+ EnvironmentMetadata environmentMetadata = new EnvironmentMetadata(config.applicationInfo, sdkMetadata, sdkKey);
+
+ // add plugin hooks
+ List allHooks = new ArrayList<>(config.hooks.getHooks());
+ for (Plugin plugin : config.plugins.getPlugins()) {
+ try {
+ allHooks.addAll(plugin.getHooks(environmentMetadata));
+ } catch (Exception e) {
+ baseLogger.error("Exception thrown getting hooks for plugin " + plugin.getMetadata().getName() + ". Unable to get hooks, plugin will not be registered.");
+ }
+ }
+ allHooks = Collections.unmodifiableList(allHooks);
+
// decorate evaluator with hooks if hooks were provided
- if (config.hooks.getHooks().isEmpty()) {
+ if (allHooks.isEmpty()) {
this.evaluator = evaluator;
this.migrationEvaluator = new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger);
} else {
- this.evaluator = new EvaluatorWithHooks(evaluator, config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
- this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), config.hooks.getHooks(), this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
+ this.evaluator = new EvaluatorWithHooks(evaluator, allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
+ this.migrationEvaluator = new EvaluatorWithHooks(new MigrationStageEnforcingEvaluator(evaluator, evaluationLogger), allHooks, this.baseLogger.subLogger(Loggers.HOOKS_LOGGER_NAME));
}
this.flagChangeBroadcaster = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor, baseLogger);
@@ -236,6 +263,15 @@ public LDClient(String sdkKey, LDConfig config) {
this.dataSource = config.dataSource.build(context.withDataSourceUpdateSink(dataSourceUpdates));
this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates);
+ // register plugins as soon as possible after client is valid
+ for (Plugin plugin : config.plugins.getPlugins()) {
+ try {
+ plugin.register(this, environmentMetadata);
+ } catch (Exception e) {
+ baseLogger.error("Exception thrown registering plugin " + plugin.getMetadata().getName() + ". Plugin will not be registered.");
+ }
+ }
+
Future startFuture = dataSource.start();
if (!config.startWait.isZero() && !config.startWait.isNegative()) {
if (!(dataSource instanceof ComponentsImpl.NullDataSource)) {
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java
index f0fac29..c840a50 100644
--- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDConfig.java
@@ -4,6 +4,7 @@
import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus;
import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder;
import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder;
+import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder;
import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
@@ -17,6 +18,7 @@
import com.launchdarkly.sdk.server.subsystems.HookConfiguration;
import com.launchdarkly.sdk.server.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
+import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
import java.time.Duration;
@@ -38,6 +40,7 @@ public final class LDConfig {
final boolean diagnosticOptOut;
final ComponentConfigurer events;
final HookConfiguration hooks;
+ final PluginsConfiguration plugins;
final ComponentConfigurer http;
final ComponentConfigurer logging;
final ServiceEndpoints serviceEndpoints;
@@ -61,6 +64,7 @@ protected LDConfig(Builder builder) {
this.dataStore = builder.dataStore == null ? Components.inMemoryDataStore() : builder.dataStore;
this.diagnosticOptOut = builder.diagnosticOptOut;
this.hooks = (builder.hooksConfigurationBuilder == null ? Components.hooks() : builder.hooksConfigurationBuilder).build();
+ this.plugins = (builder.pluginsConfigurationBuilder == null ? Components.plugins() : builder.pluginsConfigurationBuilder).build();
this.http = builder.http == null ? Components.httpConfiguration() : builder.http;
this.logging = builder.logging == null ? Components.logging() : builder.logging;
this.offline = builder.offline;
@@ -91,6 +95,7 @@ public static class Builder {
private boolean diagnosticOptOut = false;
private ComponentConfigurer events = null;
private HooksConfigurationBuilder hooksConfigurationBuilder = null;
+ private PluginsConfigurationBuilder pluginsConfigurationBuilder = null;
private ComponentConfigurer http = null;
private ComponentConfigurer logging = null;
private ServiceEndpointsBuilder serviceEndpointsBuilder = null;
@@ -120,6 +125,7 @@ public static Builder fromConfig(LDConfig config) {
newBuilder.diagnosticOptOut = config.diagnosticOptOut;
newBuilder.events = config.events;
newBuilder.hooksConfigurationBuilder = ComponentsImpl.HooksConfigurationBuilderImpl.fromHooksConfiguration(config.hooks);
+ newBuilder.pluginsConfigurationBuilder = ComponentsImpl.PluginsConfigurationBuilderImpl.fromPluginsConfiguration(config.plugins);
newBuilder.http = config.http;
newBuilder.logging = config.logging;
@@ -270,6 +276,22 @@ public Builder hooks(HooksConfigurationBuilder hooksConfiguration) {
return this;
}
+ /**
+ * Sets the SDK's plugins configuration, using a builder. This is normally a obtained from
+ * {@link Components#plugins()} ()}, which has methods for setting individual other plugin
+ * related properties.
+ *
+ * Plugin support is currently experimental and subject to change.
+ *
+ * @param pluginsConfiguration the plugins configuration builder
+ * @return the main configuration builder
+ * @see Components#plugins()
+ */
+ public Builder plugins(PluginsConfigurationBuilder pluginsConfiguration) {
+ this.pluginsConfigurationBuilder = pluginsConfiguration;
+ return this;
+ }
+
/**
* Sets the SDK's networking configuration, using a configuration builder. This builder is
* obtained from {@link Components#httpConfiguration()}, and has methods for setting individual
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EnvironmentMetadata.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EnvironmentMetadata.java
new file mode 100644
index 0000000..5672735
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/EnvironmentMetadata.java
@@ -0,0 +1,44 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
+
+/**
+ * Metadata about the environment that flag evaluations or other functionalities are being performed in.
+ */
+public final class EnvironmentMetadata {
+ private final ApplicationInfo applicationInfo;
+ private final SdkMetadata sdkMetadata;
+ private final String sdkKey;
+
+ /**
+ * @param applicationInfo for the application this SDK is used in
+ * @param sdkMetadata for the LaunchDarkly SDK
+ * @param sdkKey for the key used to initialize the SDK client
+ */
+ public EnvironmentMetadata(ApplicationInfo applicationInfo, SdkMetadata sdkMetadata, String sdkKey) {
+ this.applicationInfo = applicationInfo;
+ this.sdkMetadata = sdkMetadata;
+ this.sdkKey = sdkKey;
+ }
+
+ /**
+ * @return the {@link ApplicationInfo} for the application this SDK is used in.
+ */
+ public ApplicationInfo getApplicationInfo() {
+ return applicationInfo;
+ }
+
+ /**
+ * @return the {@link SdkMetadata} for the LaunchDarkly SDK.
+ */
+ public SdkMetadata getSdkMetadata() {
+ return sdkMetadata;
+ }
+
+ /**
+ * @return the key used to initialize the SDK client
+ */
+ public String getSdkKey() {
+ return sdkKey;
+ }
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/Plugin.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/Plugin.java
new file mode 100644
index 0000000..4504e9f
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/Plugin.java
@@ -0,0 +1,38 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.sdk.server.LDClient;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Abstract class that you can extend to create a plugin to the LaunchDarkly SDK.
+ */
+public abstract class Plugin {
+ /**
+ * @return the {@link PluginMetadata} that gives details about the plugin.
+ */
+ public abstract PluginMetadata getMetadata();
+
+ /**
+ * Registers the plugin with the SDK. Called once during SDK initialization.
+ * The SDK initialization will typically not have been completed at this point, so the plugin should take appropriate
+ * actions to ensure the SDK is ready before sending track events or evaluating flags.
+ *
+ * @param client for the plugin to use
+ * @param metadata metadata about the environment where the plugin is running.
+ */
+ public abstract void register(LDClient client, EnvironmentMetadata metadata);
+
+ /**
+ * Gets a list of hooks that the plugin wants to register.
+ * This method will be called once during SDK initialization before the register method is called.
+ * If the plugin does not need to register any hooks, this method doesn't need to be implemented.
+ *
+ * @param metadata metadata about the environment where the plugin is running.
+ * @return a list of hooks that the plugin wants to register.
+ */
+ public List getHooks(EnvironmentMetadata metadata) {
+ return Collections.emptyList();
+ }
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginMetadata.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginMetadata.java
new file mode 100644
index 0000000..de8ad61
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginMetadata.java
@@ -0,0 +1,11 @@
+package com.launchdarkly.sdk.server.integrations;
+
+/**
+ * PluginMetadata contains information about a specific plugin implementation
+ */
+public abstract class PluginMetadata {
+ /**
+ * @return the name of the plugin implementation
+ */
+ public abstract String getName();
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilder.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilder.java
new file mode 100644
index 0000000..a38b380
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilder.java
@@ -0,0 +1,51 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Contains methods for configuring the SDK's 'plugins'.
+ *
+ * If you want to add plugins, use {@link Components#plugins()}, configure accordingly, and pass it
+ * to {@link com.launchdarkly.sdk.server.LDConfig.Builder#plugins(PluginsConfigurationBuilder)}.
+ *
+ *
+ * List plugins = getPluginsFunc();
+ * LDConfig config = new LDConfig.Builder()
+ * .plugins(
+ * Components.plugins()
+ * .setPlugins(plugins)
+ * )
+ * .build();
+ *
+ *
+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#plugins()}.
+ */
+public abstract class PluginsConfigurationBuilder {
+ /**
+ * The current set of plugins the builder has.
+ */
+ protected List plugins = Collections.emptyList();
+
+ /**
+ * Sets the provided list of plugins on the configuration. Note that the order of plugins is important and controls
+ * the order in which they will be registered. See {@link Plugin} for more details.
+ *
+ * @param plugins to be set on the configuration
+ * @return the builder
+ */
+ public PluginsConfigurationBuilder setPlugins(List plugins) {
+ // copy to avoid list manipulations impacting the SDK
+ this.plugins = Collections.unmodifiableList(new ArrayList<>(plugins));
+ return this;
+ }
+
+ /**
+ * @return the plugins configuration
+ */
+ public abstract PluginsConfiguration build();
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/SdkMetadata.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/SdkMetadata.java
new file mode 100644
index 0000000..6623d74
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/integrations/SdkMetadata.java
@@ -0,0 +1,50 @@
+package com.launchdarkly.sdk.server.integrations;
+
+/**
+ * Metadata about the LaunchDarkly SDK.
+ */
+public final class SdkMetadata {
+ private final String name;
+ private final String version;
+ private final String wrapperName;
+ private final String wrapperVersion;
+
+ public SdkMetadata(String name, String version) {
+ this(name, version, null, null);
+ }
+
+ public SdkMetadata(String name, String version, String wrapperName, String wrapperVersion) {
+ this.name = name;
+ this.version = version;
+ this.wrapperName = wrapperName;
+ this.wrapperVersion = wrapperVersion;
+ }
+
+ /**
+ * @return name of the SDK for informational purposes such as logging
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return version of the SDK for informational purposes such as logging
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * @return name of the wrapper if this is a wrapper SDK
+ */
+ public String getWrapperName() {
+ return wrapperName;
+ }
+
+ /**
+ * @return version of the wrapper if this is a wrapper SDK
+ */
+ public String getWrapperVersion() {
+ return wrapperVersion;
+ }
+}
diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/PluginsConfiguration.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/PluginsConfiguration.java
new file mode 100644
index 0000000..8bd3585
--- /dev/null
+++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/PluginsConfiguration.java
@@ -0,0 +1,30 @@
+package com.launchdarkly.sdk.server.subsystems;
+
+import com.launchdarkly.sdk.server.integrations.Plugin;
+import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates the SDK's 'plugins' configuration.
+ *
+ * Use {@link PluginsConfigurationBuilder} to construct an instance.
+ */
+public class PluginsConfiguration {
+ private final List plugins;
+
+ /**
+ * @param plugins the list of {@link Plugin} that will be registered.
+ */
+ public PluginsConfiguration(List plugins) {
+ this.plugins = Collections.unmodifiableList(plugins);
+ }
+
+ /**
+ * @return immutable list of plugins
+ */
+ public List getPlugins() {
+ return plugins;
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientPluginsTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientPluginsTest.java
new file mode 100644
index 0000000..c2626a7
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientPluginsTest.java
@@ -0,0 +1,234 @@
+package com.launchdarkly.sdk.server;
+
+import com.launchdarkly.sdk.LDContext;
+import com.launchdarkly.sdk.server.integrations.EnvironmentMetadata;
+import com.launchdarkly.sdk.server.integrations.Hook;
+import com.launchdarkly.sdk.server.integrations.Plugin;
+import com.launchdarkly.sdk.server.integrations.PluginMetadata;
+import com.launchdarkly.sdk.server.integrations.SdkMetadata;
+import com.launchdarkly.sdk.server.interfaces.ApplicationInfo;
+
+import org.junit.Test;
+import org.easymock.IArgumentMatcher;
+
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.mock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.Objects;
+
+@SuppressWarnings("javadoc")
+public class LDClientPluginsTest extends BaseTest {
+ private static EnvironmentMetadata envMetaEquals(EnvironmentMetadata metadata) {
+ reportMatcher(new IArgumentMatcher() {
+ @Override
+ public boolean matches(Object argument) {
+ if (metadata == argument) {
+ return true;
+ }
+ if (metadata == null || argument == null || !(argument instanceof EnvironmentMetadata)) {
+ return false;
+ }
+ EnvironmentMetadata target = (EnvironmentMetadata)argument;
+ return (
+ Objects.equals(metadata.getSdkKey(), target.getSdkKey()) &&
+ Objects.equals(metadata.getSdkMetadata().getName(), target.getSdkMetadata().getName()) &&
+ Objects.equals(metadata.getSdkMetadata().getVersion(), target.getSdkMetadata().getVersion()) &&
+ Objects.equals(metadata.getSdkMetadata().getWrapperName(), target.getSdkMetadata().getWrapperName()) &&
+ Objects.equals(metadata.getSdkMetadata().getWrapperVersion(), target.getSdkMetadata().getWrapperVersion()) &&
+ Objects.equals(metadata.getApplicationInfo().getApplicationId(), target.getApplicationInfo().getApplicationId()) &&
+ Objects.equals(metadata.getApplicationInfo().getApplicationVersion(), target.getApplicationInfo().getApplicationVersion())
+ );
+ }
+ @Override
+ public void appendTo(StringBuffer buffer) {
+ buffer.append("envMetaEquals()");
+ }
+ });
+ return null;
+ }
+
+ private final static String SDK_KEY = "SDK_KEY";
+
+ @Test
+ public void pluginsAreProvidedEnvironmentMetadata() throws Exception {
+ EnvironmentMetadata expected = new EnvironmentMetadata(
+ new ApplicationInfo(null, null),
+ new SdkMetadata("JavaClient", Version.SDK_VERSION),
+ SDK_KEY
+ );
+
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(envMetaEquals(expected))).andReturn(Collections.emptyList());
+ mockPlugin.register(isA(LDClient.class), envMetaEquals(expected));
+ expectLastCall();
+ replay(mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ verify(mockPlugin);
+ }
+ }
+
+ @Test
+ public void pluginsAreProvidedEnvironmentMetadataWithApplicationInfo() throws Exception {
+ EnvironmentMetadata expected = new EnvironmentMetadata(
+ new ApplicationInfo("app-id", "app-version"),
+ new SdkMetadata("JavaClient", Version.SDK_VERSION),
+ SDK_KEY
+ );
+
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(envMetaEquals(expected))).andReturn(Collections.emptyList());
+ mockPlugin.register(isA(LDClient.class), envMetaEquals(expected));
+ expectLastCall();
+ replay(mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .applicationInfo(Components.applicationInfo().applicationId("app-id").applicationVersion("app-version"))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ verify(mockPlugin);
+ }
+ }
+
+ @Test
+ public void pluginsAreProvidedEnvironmentMetadataWithWrapper() throws Exception {
+ EnvironmentMetadata expected = new EnvironmentMetadata(
+ new ApplicationInfo(null, null),
+ new SdkMetadata("JavaClient", Version.SDK_VERSION, "wrapper-name", "wrapper-version"),
+ SDK_KEY
+ );
+
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(envMetaEquals(expected))).andReturn(Collections.emptyList());
+ mockPlugin.register(isA(LDClient.class), envMetaEquals(expected));
+ expectLastCall();
+ replay(mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .wrapper(Components.wrapperInfo().wrapperName("wrapper-name").wrapperVersion("wrapper-version"))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ verify(mockPlugin);
+ }
+ }
+
+ @Test
+ public void pluginHooksAreRegistered() throws Exception {
+ Hook mockHook = mock(Hook.class);
+ expect(mockHook.beforeEvaluation(anyObject(), anyObject())).andReturn(Collections.emptyMap());
+ expect(mockHook.afterEvaluation(anyObject(), anyObject(), anyObject())).andReturn(Collections.emptyMap());
+
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(anyObject())).andReturn(Collections.singletonList(mockHook));
+ mockPlugin.register(anyObject(), anyObject());
+ expectLastCall();
+
+ replay(mockHook, mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ client.boolVariation(SDK_KEY, LDContext.create("test-context"), false);
+ verify(mockHook, mockPlugin);
+ }
+ }
+
+ @Test
+ public void handlesExceptionInGetHooks() throws Exception {
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(anyObject())).andThrow(new RuntimeException("test error"));
+ expect(mockPlugin.getMetadata()).andReturn(new PluginMetadata() {
+ @Override
+ public String getName() {
+ return "TestPlugin";
+ }
+ });
+ mockPlugin.register(anyObject(), anyObject());
+ expectLastCall();
+
+ replay(mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ assertTrue(
+ logCapture.getMessageStrings().contains(
+ "ERROR:Exception thrown getting hooks for plugin TestPlugin. Unable to get hooks, plugin will not be registered."
+ )
+ );
+ verify(mockPlugin);
+ }
+ }
+
+ @Test
+ public void pluginHooksAreRegisteredWithExistingHooks() throws Exception {
+ Hook mockExistingHook = mock(Hook.class);
+ expect(mockExistingHook.beforeEvaluation(anyObject(), anyObject())).andReturn(Collections.emptyMap());
+ expect(mockExistingHook.afterEvaluation(anyObject(), anyObject(), anyObject())).andReturn(Collections.emptyMap());
+
+ Hook mockPluginHook = mock(Hook.class);
+ expect(mockPluginHook.beforeEvaluation(anyObject(), anyObject())).andReturn(Collections.emptyMap());
+ expect(mockPluginHook.afterEvaluation(anyObject(), anyObject(), anyObject())).andReturn(Collections.emptyMap());
+
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(anyObject())).andReturn(Collections.singletonList(mockPluginHook));
+ mockPlugin.register(anyObject(), anyObject());
+ expectLastCall();
+
+ replay(mockExistingHook, mockPluginHook, mockPlugin);
+
+ LDConfig config = baseConfig()
+ .hooks(Components.hooks().setHooks(Collections.singletonList(mockExistingHook)))
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ client.boolVariation(SDK_KEY, LDContext.create("test-context"), false);
+ verify(mockExistingHook, mockPluginHook, mockPlugin);
+ }
+ }
+
+ @Test
+ public void handlesExceptionInRegister() throws Exception {
+ Plugin mockPlugin = mock(Plugin.class);
+ expect(mockPlugin.getHooks(anyObject())).andReturn(Collections.emptyList());
+ expect(mockPlugin.getMetadata()).andReturn(new PluginMetadata() {
+ @Override
+ public String getName() {
+ return "TestPlugin";
+ }
+ });
+ mockPlugin.register(anyObject(), anyObject());
+ expectLastCall().andThrow(new RuntimeException("test error"));
+
+ replay(mockPlugin);
+
+ LDConfig config = baseConfig()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+
+ try (LDClient client = new LDClient(SDK_KEY, config)) {
+ assertTrue(
+ logCapture.getMessageStrings().contains(
+ "ERROR:Exception thrown registering plugin TestPlugin. Plugin will not be registered."
+ )
+ );
+ verify(mockPlugin);
+ }
+ }
+}
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java
index af894e8..a5540ac 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java
@@ -3,6 +3,7 @@
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.server.integrations.MockPersistentDataStore;
+import com.launchdarkly.sdk.server.integrations.Plugin;
import com.launchdarkly.sdk.server.integrations.Hook;
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
@@ -31,9 +32,12 @@
import static com.launchdarkly.sdk.server.TestComponents.initedDataStore;
import static com.launchdarkly.sdk.server.TestComponents.specificComponent;
import static com.launchdarkly.sdk.server.TestUtil.upsertFlag;
+import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.mock;
+import static org.easymock.EasyMock.niceMock;
+import static org.easymock.EasyMock.replay;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
@@ -229,6 +233,17 @@ public void canSetHooks() throws Exception {
try (LDClient client = new LDClient(SDK_KEY, config2)) {
assertEquals(EvaluatorWithHooks.class, client.evaluator.getClass());
}
+
+ Plugin mockPlugin = niceMock(Plugin.class);
+ expect(mockPlugin.getHooks(anyObject())).andReturn(Collections.singletonList(mock(Hook.class)));
+ replay(mockPlugin);
+
+ LDConfig config3 = new LDConfig.Builder()
+ .plugins(Components.plugins().setPlugins(Collections.singletonList(mockPlugin)))
+ .build();
+ try (LDClient client = new LDClient(SDK_KEY, config3)) {
+ assertEquals(EvaluatorWithHooks.class, client.evaluator.getClass());
+ }
}
@Test
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
index 100cac1..5a062fb 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java
@@ -6,6 +6,8 @@
import com.launchdarkly.sdk.server.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder;
+import com.launchdarkly.sdk.server.integrations.Plugin;
+import com.launchdarkly.sdk.server.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder;
import com.launchdarkly.sdk.server.integrations.Hook;
@@ -128,6 +130,15 @@ public void hooks() {
assertEquals(mockHook, config.hooks.getHooks().get(0));
}
+ @Test
+ public void plugins() {
+ Plugin mockPlugin = mock(Plugin.class);
+ PluginsConfigurationBuilder b = Components.plugins().setPlugins(Collections.singletonList(mockPlugin));
+ LDConfig config = new LDConfig.Builder().plugins(b).build();
+ assertEquals(1, config.plugins.getPlugins().size());
+ assertEquals(mockPlugin, config.plugins.getPlugins().get(0));
+ }
+
@Test
public void http() {
HttpConfigurationBuilder b = Components.httpConfiguration().connectTimeout(Duration.ofSeconds(9));
@@ -197,6 +208,8 @@ public void fromConfig() {
ComponentConfigurer eventProcessor = specificComponent(null);
Hook mockHook = mock(Hook.class);
HooksConfigurationBuilder hooksBuilder = Components.hooks().setHooks(Collections.singletonList(mockHook));
+ Plugin mockPlugin = mock(Plugin.class);
+ PluginsConfigurationBuilder pluginsBuilder = Components.plugins().setPlugins(Collections.singletonList(mockPlugin));
HttpConfigurationBuilder http = Components.httpConfiguration().connectTimeout(Duration.ofSeconds(9));
WrapperInfoBuilder wrapperInfo = Components.wrapperInfo().wrapperName("the-name").wrapperVersion("the-version");
ApplicationInfoBuilder applicationInfo = Components.applicationInfo().applicationId("test").applicationVersion("version");
@@ -211,6 +224,7 @@ public void fromConfig() {
.diagnosticOptOut(true)
.offline(false) // To keep the data source from being removed in the build.
.hooks(hooksBuilder)
+ .plugins(pluginsBuilder)
.http(http)
.serviceEndpoints(serviceEndpoints)
.wrapper(wrapperInfo).build();
@@ -233,6 +247,7 @@ public void fromConfig() {
assertEquals(URI.create("events"), config2.serviceEndpoints.getEventsBaseUri());
assertEquals(mockHook, config2.hooks.getHooks().get(0));
+ assertEquals(mockPlugin, config2.plugins.getPlugins().get(0));
}
@Test
@@ -252,6 +267,9 @@ public void fromConfigDefault() {
assertNotNull(config.hooks.getHooks());
assertEquals(0, config.hooks.getHooks().size());
+ assertNotNull(config.plugins.getPlugins());
+ assertEquals(0, config.plugins.getPlugins().size());
+
assertNotNull(config.http);
HttpConfiguration httpConfig = config.http.build(BASIC_CONTEXT);
assertEquals(HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT, httpConfig.getConnectTimeout());
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilderTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilderTest.java
new file mode 100644
index 0000000..b9fb864
--- /dev/null
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/integrations/PluginsConfigurationBuilderTest.java
@@ -0,0 +1,29 @@
+package com.launchdarkly.sdk.server.integrations;
+
+import com.launchdarkly.sdk.server.Components;
+import com.launchdarkly.sdk.server.subsystems.PluginsConfiguration;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.easymock.EasyMock.mock;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+public class PluginsConfigurationBuilderTest {
+ @Test
+ public void emptyPluginsAsDefault() {
+ PluginsConfiguration configuration = Components.plugins().build();
+ assertEquals(0, configuration.getPlugins().size());
+ }
+
+ @Test
+ public void canSetPlugins() {
+ Plugin pluginA = mock(Plugin.class);
+ Plugin pluginB = mock(Plugin.class);
+ PluginsConfiguration configuration = Components.plugins().setPlugins(Arrays.asList(pluginA, pluginB)).build();
+ assertEquals(2, configuration.getPlugins().size());
+ assertSame(pluginA, configuration.getPlugins().get(0));
+ assertSame(pluginB, configuration.getPlugins().get(1));
+ }
+}