Skip to content

feat: add mechanism for open-close-principle to OpenFeatureApi #1015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
146 changes: 92 additions & 54 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;

Expand All @@ -35,10 +38,6 @@
transactionContextPropagator = new NoOpTransactionContextPropagator();
}

private static class SingletonHolder {
private static final OpenFeatureAPI INSTANCE = new OpenFeatureAPI();
}

/**
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
*
Expand Down Expand Up @@ -86,7 +85,7 @@
* Multiple clients can be used to segment feature flag configuration.
* If there is already a provider bound to this domain, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
*
*
* @param domain an identifier which logically binds clients with providers
* @return a new client instance
*/
Expand All @@ -100,8 +99,8 @@
* Multiple clients can be used to segment feature flag configuration.
* If there is already a provider bound to this domain, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
*
* @param domain a identifier which logically binds clients with providers
*
* @param domain a identifier which logically binds clients with providers
* @param version a version identifier
* @return a new client instance
*/
Expand All @@ -111,6 +110,17 @@
version);
}

/**
* Gets the global evaluation context, which will be used for all evaluations.
*
* @return evaluation context
*/
public EvaluationContext getEvaluationContext() {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
return this.evaluationContext;
}
}

/**
* Sets the global evaluation context, which will be used for all evaluations.
*
Expand All @@ -124,17 +134,6 @@
return this;
}

/**
* Gets the global evaluation context, which will be used for all evaluations.
*
* @return evaluation context
*/
public EvaluationContext getEvaluationContext() {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
return this.evaluationContext;
}
}

/**
* Return the transaction context propagator.
*/
Expand Down Expand Up @@ -175,39 +174,6 @@
this.transactionContextPropagator.setTransactionContext(evaluationContext);
}

/**
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
}
}

/**
* Add a provider for a domain.
*
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProvider(String domain, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
}
}

/**
* Set the default provider and wait for initialization to finish.
*/
Expand All @@ -226,8 +192,8 @@
/**
* Add a provider for a domain and wait for initialization to finish.
*
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand Down Expand Up @@ -286,6 +252,39 @@
return providerRepository.getProvider(domain);
}

/**
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
}
}

/**
* Add a provider for a domain.
*
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProvider(String domain, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
}
}

/**
* Adds hooks for globally, used for all evaluations.
* Hooks are run in the order they're added in the before stage. They are run in reverse order for all other stages.
Expand All @@ -300,6 +299,7 @@

/**
* Fetch the hooks associated to this client.
*
* @return A list of {@link Hook}s.
*/
public List<Hook> getHooks() {
Expand Down Expand Up @@ -404,7 +404,7 @@

/**
* Runs the handlers associated with a particular provider.
*
*
* @param provider the provider from where this event originated
* @param event the event type
* @param details the event details
Expand Down Expand Up @@ -440,4 +440,42 @@
}
}
}

private static class SingletonHolder {
private static final OpenFeatureAPI INSTANCE;

static {
OpenFeatureAPI instance = null;
String cls = System.getenv("OPEN_FEATURE_API_CLASS");
if (cls == null) {
try (InputStream propertiesFile =
SingletonHolder.class.getResourceAsStream("openfeature.properties")) {
if (propertiesFile != null) {
Properties props = new Properties();
props.load(propertiesFile);
cls = props.getProperty("openfeature.api.class");

Check warning on line 456 in src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java#L454-L456

Added lines #L454 - L456 were not covered by tests
}
} catch (IOException e) {
log.error("Custom OpenFeatureApi could not be initialized", e);

Check warning on line 459 in src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java#L458-L459

Added lines #L458 - L459 were not covered by tests
}
}
if (cls != null) {
try {
Class<?> clazz = Class.forName(cls);

Check warning on line 464 in src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java#L464

Added line #L464 was not covered by tests

instance = (OpenFeatureAPI) clazz.newInstance();
} catch (ClassNotFoundException

Check warning on line 467 in src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java#L466-L467

Added lines #L466 - L467 were not covered by tests
| ClassCastException
| InstantiationException
| IllegalAccessException e) {
log.error("Custom OpenFeatureApi could not be initialized", e);
}

Check warning on line 472 in src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java#L471-L472

Added lines #L471 - L472 were not covered by tests
}
if (instance == null) {
instance = new OpenFeatureAPI();
}

INSTANCE = instance;
}
}
}
Loading