diff --git a/pom.xml b/pom.xml
index 1ec6e791e..e147779e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,7 @@
hooks/open-telemetry
providers/flagd
+ providers/go-feature-flag
@@ -45,7 +46,7 @@
-
+
dev.openfeature
javasdk
diff --git a/providers/go-feature-flag/README.md b/providers/go-feature-flag/README.md
new file mode 100644
index 000000000..3b75b01c0
--- /dev/null
+++ b/providers/go-feature-flag/README.md
@@ -0,0 +1,30 @@
+# GO Feature Flag Java Provider
+
+GO Feature Flag provider allows you to connect to your [GO Feature Flag relay proxy](https://gofeatureflag.org) instance.
+
+## How to use this provider?
+
+To initialize your instance please follow this example:
+
+```java
+import dev.openfeature.contrib.providers.gofeatureflag;
+
+// ...
+new GoFeatureFlagProvider(
+ GoFeatureFlagProviderOptions
+ .builder()
+ .endpoint("https://my-gofeatureflag-instance.org")
+ .timeout(1000)
+ .build());
+```
+
+You will have a new instance ready to be used with your `open-feature` java SDK.
+
+### Options
+
+| name | mandatory | Description |
+|--------------------------|-----------|----------------------------------------------------------------------------------------------------------------|
+| **`endpoint`** | `true` | endpoint contains the DNS of your GO Feature Flag relay proxy _(ex: https://mydomain.com/gofeatureflagproxy/)_ |
+| **`timeout`** | `false` | timeout in millisecond we are waiting when calling the go-feature-flag relay proxy API. _(default: 10000)_ |
+| **`maxIdleConnections`** | `false` | maxIdleConnections is the maximum number of connexions in the connexion pool. _(default: 1000)_ |
+| **`keepAliveDuration`** | `false` | keepAliveDuration is the time in millisecond we keep the connexion open. _(default: 7200000 (2 hours))_ |
diff --git a/providers/go-feature-flag/lombok.config b/providers/go-feature-flag/lombok.config
new file mode 100644
index 000000000..bcd1afdae
--- /dev/null
+++ b/providers/go-feature-flag/lombok.config
@@ -0,0 +1,5 @@
+# This file is needed to avoid errors throw by findbugs when working with lombok.
+lombok.addSuppressWarnings = true
+lombok.addLombokGeneratedAnnotation = true
+config.stopBubbling = true
+lombok.extern.findbugs.addSuppressFBWarnings = true
diff --git a/providers/go-feature-flag/pom.xml b/providers/go-feature-flag/pom.xml
new file mode 100644
index 000000000..80ab079c3
--- /dev/null
+++ b/providers/go-feature-flag/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ dev.openfeature.contrib
+ parent
+ 0.0.2
+ ../../pom.xml
+
+ dev.openfeature.contrib.providers
+ go-feature-flag
+ 0.1.0
+
+ go-feature-flag
+ GO Feature Flag provider for Java
+ https://gofeatureflag.org
+
+
+
+ thomaspoignant
+ Thomas Poignant
+ go-feature-flag
+ https://gofeatureflag.org
+
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.13.4
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.4
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.10.0
+
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.10.0
+ test
+
+
+
diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java
new file mode 100644
index 000000000..33d7d1635
--- /dev/null
+++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java
@@ -0,0 +1,312 @@
+package dev.openfeature.contrib.providers.gofeatureflag;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import dev.openfeature.contrib.providers.gofeatureflag.bean.GoFeatureFlagRequest;
+import dev.openfeature.contrib.providers.gofeatureflag.bean.GoFeatureFlagResponse;
+import dev.openfeature.contrib.providers.gofeatureflag.bean.GoFeatureFlagUser;
+import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
+import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
+import dev.openfeature.javasdk.ErrorCode;
+import dev.openfeature.javasdk.EvaluationContext;
+import dev.openfeature.javasdk.FeatureProvider;
+import dev.openfeature.javasdk.Hook;
+import dev.openfeature.javasdk.Metadata;
+import dev.openfeature.javasdk.ProviderEvaluation;
+import dev.openfeature.javasdk.Reason;
+import dev.openfeature.javasdk.Structure;
+import dev.openfeature.javasdk.Value;
+import dev.openfeature.javasdk.exceptions.FlagNotFoundError;
+import dev.openfeature.javasdk.exceptions.GeneralError;
+import dev.openfeature.javasdk.exceptions.OpenFeatureError;
+import dev.openfeature.javasdk.exceptions.TypeMismatchError;
+import okhttp3.ConnectionPool;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+
+/**
+ * GoFeatureFlagProvider is the JAVA provider implementation for the feature flag solution GO Feature Flag.
+ */
+public class GoFeatureFlagProvider implements FeatureProvider {
+ private static final String NAME = "GO Feature Flag Provider";
+ private static final ObjectMapper requestMapper = new ObjectMapper();
+ private static final ObjectMapper responseMapper = new ObjectMapper();
+ private HttpUrl parsedEndpoint;
+ // httpClient is the instance of the OkHttpClient used by the provider
+ private OkHttpClient httpClient;
+
+ /**
+ * Constructor of the provider.
+ *
+ * @param options - options to configure the provider
+ * @throws InvalidOptions - if options are invalid
+ */
+ public GoFeatureFlagProvider(GoFeatureFlagProviderOptions options) throws InvalidOptions {
+ this.validateInputOptions(options);
+ this.initializeProvider(options);
+ }
+
+
+ /**
+ * validateInputOptions is validating the different options provided when creating the provider.
+ *
+ * @param options - Options used while creating the provider
+ * @throws InvalidOptions - if no options are provided
+ * @throws InvalidEndpoint - if the endpoint provided is not valid
+ */
+ private void validateInputOptions(GoFeatureFlagProviderOptions options) throws InvalidEndpoint, InvalidOptions {
+ if (options == null) {
+ throw new InvalidOptions("No options provided");
+ }
+
+ if (options.getEndpoint() == null || "".equals(options.getEndpoint())) {
+ throw new InvalidEndpoint("endpoint is a mandatory field when initializing the provider");
+ }
+ }
+
+ /**
+ * initializeProvider is initializing the different class element used by the provider.
+ *
+ * @param options - Options used while creating the provider
+ */
+ private void initializeProvider(GoFeatureFlagProviderOptions options) throws InvalidEndpoint {
+ // Register JavaTimeModule to be able to deserialized java.time.Instant Object
+ requestMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ requestMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ requestMapper.registerModule(new JavaTimeModule());
+
+ // init httpClient to call the GO Feature Flag API
+ int timeout = options.getTimeout() == 0 ? 10000 : options.getTimeout();
+ long keepAliveDuration = options.getKeepAliveDuration() == null ? 7200000 : options.getKeepAliveDuration();
+ int maxIdleConnections = options.getMaxIdleConnections() == 0 ? 1000 : options.getMaxIdleConnections();
+ this.httpClient = new OkHttpClient.Builder()
+ .connectTimeout(timeout, TimeUnit.MILLISECONDS)
+ .readTimeout(timeout, TimeUnit.MILLISECONDS)
+ .callTimeout(timeout, TimeUnit.MILLISECONDS)
+ .readTimeout(timeout, TimeUnit.MILLISECONDS)
+ .writeTimeout(timeout, TimeUnit.MILLISECONDS)
+ .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS))
+ .build();
+
+ this.parsedEndpoint = HttpUrl.parse(options.getEndpoint());
+ if (this.parsedEndpoint == null) {
+ throw new InvalidEndpoint();
+ }
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return () -> NAME;
+ }
+
+ @Override
+ public List getProviderHooks() {
+ return FeatureProvider.super.getProviderHooks();
+ }
+
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(
+ String key, Boolean defaultValue, EvaluationContext evaluationContext
+ ) {
+ return resolveEvaluationGoFeatureFlagProxy(key, defaultValue, evaluationContext, Boolean.class);
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(
+ String key, String defaultValue, EvaluationContext evaluationContext
+ ) {
+ return resolveEvaluationGoFeatureFlagProxy(key, defaultValue, evaluationContext, String.class);
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(
+ String key, Integer defaultValue, EvaluationContext evaluationContext
+ ) {
+ return resolveEvaluationGoFeatureFlagProxy(key, defaultValue, evaluationContext, Integer.class);
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(
+ String key, Double defaultValue, EvaluationContext evaluationContext
+ ) {
+ return resolveEvaluationGoFeatureFlagProxy(key, defaultValue, evaluationContext, Double.class);
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(
+ String key, Value defaultValue, EvaluationContext evaluationContext
+ ) {
+ return resolveEvaluationGoFeatureFlagProxy(key, defaultValue, evaluationContext, Value.class);
+ }
+
+ /**
+ * resolveEvaluationGoFeatureFlagProxy is calling the GO Feature Flag API to retrieve the flag value.
+ *
+ * @param key - name of the feature flag
+ * @param defaultValue - value used if something is not working as expected
+ * @param ctx - EvaluationContext used for the request
+ * @param expectedType - type expected for the value
+ * @return a ProviderEvaluation that contains the open-feature response
+ * @throws OpenFeatureError - if an error happen
+ */
+ private ProviderEvaluation resolveEvaluationGoFeatureFlagProxy(
+ String key, T defaultValue, EvaluationContext ctx, Class> expectedType
+ ) throws OpenFeatureError {
+ try {
+ GoFeatureFlagUser user = GoFeatureFlagUser.fromEvaluationContext(ctx);
+ GoFeatureFlagRequest goffRequest = new GoFeatureFlagRequest(user, defaultValue);
+
+ HttpUrl url = this.parsedEndpoint.newBuilder()
+ .addEncodedPathSegment("v1")
+ .addEncodedPathSegment("feature")
+ .addEncodedPathSegment(key)
+ .addEncodedPathSegment("eval")
+ .build();
+
+ Request request = new Request.Builder()
+ .url(url)
+ .addHeader("Content-Type", "application/json")
+ .post(RequestBody.create(
+ requestMapper.writeValueAsBytes(goffRequest),
+ MediaType.get("application/json; charset=utf-8")))
+ .build();
+
+ try (Response response = this.httpClient.newCall(request).execute()) {
+ if (response.code() >= HTTP_BAD_REQUEST) {
+ throw new GeneralError("impossible to contact GO Feature Flag relay proxy instance");
+ }
+
+ ResponseBody responseBody = response.body();
+ String body = responseBody != null ? responseBody.string() : "";
+ GoFeatureFlagResponse goffResp =
+ responseMapper.readValue(body, GoFeatureFlagResponse.class);
+
+ if (Reason.DISABLED.name().equalsIgnoreCase(goffResp.getReason())) {
+ // we don't set a variant since we are using the default value, and we are not able to know
+ // which variant it is.
+ return ProviderEvaluation.builder().value(defaultValue).reason(Reason.DISABLED).build();
+ }
+
+ if (ErrorCode.FLAG_NOT_FOUND.name().equalsIgnoreCase(goffResp.getErrorCode())) {
+ throw new FlagNotFoundError("Flag " + key + " was not found in your configuration");
+ }
+
+ // Convert the value received from the API.
+ T flagValue = convertValue(goffResp.getValue(), expectedType);
+
+ if (flagValue.getClass() != expectedType) {
+ throw new TypeMismatchError("Flag value " + key + " had unexpected type "
+ + flagValue.getClass() + ", expected " + expectedType + ".");
+ }
+
+ return ProviderEvaluation.builder()
+ .errorCode(goffResp.getErrorCode())
+ .reason(mapReason(goffResp.getReason()))
+ .value(flagValue)
+ .variant(goffResp.getVariationType())
+ .build();
+
+ }
+ } catch (IOException e) {
+ throw new GeneralError("unknown error while retrieving flag " + key);
+ }
+ }
+
+
+ /**
+ * mapReason is mapping the reason in string received by the API to our internal SDK reason enum.
+ *
+ * @param reason - string of the reason received from the API
+ * @return an item from the enum
+ */
+ private Reason mapReason(String reason) {
+ try {
+ return Reason.valueOf(reason);
+ } catch (IllegalArgumentException e) {
+ return Reason.UNKNOWN;
+ }
+ }
+
+
+ /**
+ * convertValue is converting the object return by the proxy response in the right type.
+ *
+ * @param value - The value we have received
+ * @param expectedType - the type we expect for this value
+ * @param the type we want to convert to.
+ * @return A converted object
+ */
+ private T convertValue(Object value, Class> expectedType) {
+ boolean isPrimitive = expectedType == Boolean.class
+ || expectedType == String.class
+ || expectedType == Integer.class
+ || expectedType == Double.class;
+
+ if (isPrimitive) {
+ return (T) value;
+ }
+ return (T) objectToValue(value);
+ }
+
+ /**
+ * objectToValue is wrapping an object into a Value.
+ *
+ * @param object the object you want to wrap
+ * @return the wrapped object
+ */
+ private Value objectToValue(Object object) {
+ if (object instanceof Value) {
+ return (Value) object;
+ } else if (object == null) {
+ return null;
+ } else if (object instanceof String) {
+ return new Value((String) object);
+ } else if (object instanceof Boolean) {
+ return new Value((Boolean) object);
+ } else if (object instanceof Integer) {
+ return new Value((Integer) object);
+ } else if (object instanceof Double) {
+ return new Value((Double) object);
+ } else if (object instanceof Structure) {
+ return new Value((Structure) object);
+ } else if (object instanceof List) {
+ // need to translate each elem in list to a value
+ return new Value(((List