diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index c5641fde7..28697894a 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -37,6 +37,9 @@ components:
- liran2000
providers/multiprovider:
- liran2000
+ providers/ofrep-provider:
+ - Rahul-Baradol
+ - toddbaert
tools/flagd-http-connector:
- liran2000
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 84e9a85a2..54270dc9a 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -10,6 +10,7 @@
"providers/configcat": "0.2.1",
"providers/statsig": "0.2.1",
"providers/multiprovider": "0.0.3",
+ "providers/ofrep": "0.0.1",
"tools/junit-openfeature": "0.2.1",
"tools/flagd-http-connector": "0.0.4",
".": "1.0.0"
diff --git a/pom.xml b/pom.xml
index eab875784..0568bef07 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
providers/configcat
providers/statsig
providers/multiprovider
+ providers/ofrep
tools/flagd-http-connector
diff --git a/providers/ofrep/README.md b/providers/ofrep/README.md
new file mode 100644
index 000000000..d03cd79df
--- /dev/null
+++ b/providers/ofrep/README.md
@@ -0,0 +1,74 @@
+# OFREP Provider for OpenFeature
+
+This provider allows to connect to any feature flag management system that supports OFREP.
+
+## Installation
+For Maven
+
+```xml
+
+ dev.openfeature.contrib.providers
+ ofrep
+ 0.0.1
+
+```
+
+For Gradle
+```groovy
+implementation 'dev.openfeature.contrib.providers:ofrep:0.0.1'
+```
+
+
+## Configuration and Usage
+
+### Usage
+```java
+OfrepProviderOptions options = OfrepProviderOptions.builder().build();
+OfrepProvider ofrepProvider = OfrepProvider.constructProvider(options);
+```
+### Example
+```java
+import dev.openfeature.contrib.providers.ofrep.OfrepProvider;
+import dev.openfeature.contrib.providers.ofrep.OfrepProviderOptions;
+import dev.openfeature.sdk.Client;
+import dev.openfeature.sdk.FlagEvaluationDetails;
+import dev.openfeature.sdk.MutableContext;
+import dev.openfeature.sdk.OpenFeatureAPI;
+
+public class App {
+ public static void main(String[] args) {
+ OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance();
+
+ OfrepProviderOptions options = OfrepProviderOptions.builder().build();
+ OfrepProvider ofrepProvider = OfrepProvider.constructProvider(options);
+
+ openFeatureAPI.setProvider(ofrepProvider);
+
+ Client client = openFeatureAPI.getClient();
+
+ MutableContext context = new MutableContext();
+ context.setTargetingKey("my-identify-id");
+
+ FlagEvaluationDetails details = client.getBooleanDetails("my-boolean-flag", false, context);
+ System.out.println("Flag value: " + details.getValue());
+
+ openFeatureAPI.shutdown();
+ }
+}
+```
+
+### Configuration options
+
+Options are passed via `OfrepProviderOptions`, using which default values can be overridden.
+
+Given below are the supported configurations:
+
+
+| Option name | Type | Default | Description
+| ----------- | ------- | --------- | ---------
+| baseUrl | String | http://localhost:8016 | Override the default OFREP API URL.
+| headers | ImmutableMap | Empty Map | Add custom headers which will be sent with each network request to the OFREP API.
+| timeout | Duration | 10 Seconds | The timeout duration to establishing the connection.
+| proxySelector | ProxySelector | ProxySelector.getDefault() | The proxy selector used by HTTP Client.
+| executor | Executor | Thread Pool of size 5 | The executor used by HTTP Client.
+
diff --git a/providers/ofrep/pom.xml b/providers/ofrep/pom.xml
new file mode 100644
index 000000000..564b83d89
--- /dev/null
+++ b/providers/ofrep/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ dev.openfeature.contrib
+ parent
+ 1.0.0
+ ../../pom.xml
+
+
+ dev.openfeature.contrib.providers
+ ofrep
+ 0.0.1
+
+ ofrep
+ OFREP Provider
+ https://openfeature.dev
+
+
+
+ Rahul-Baradol
+ Rahul Baradol
+ OpenFeature
+ https://openfeature.dev/
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.19.1
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.19.1
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.19.1
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.19.1
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.17
+
+
+
+ commons-validator
+ commons-validator
+ 1.7
+
+
+
+ com.google.guava
+ guava
+ 33.4.0-jre
+
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.12.0
+ test
+
+
+
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProvider.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProvider.java
new file mode 100644
index 000000000..918b570f0
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProvider.java
@@ -0,0 +1,139 @@
+package dev.openfeature.contrib.providers.ofrep;
+
+import dev.openfeature.contrib.providers.ofrep.internal.Resolver;
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.Metadata;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Value;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.validator.routines.UrlValidator;
+
+/**
+ * OpenFeature provider for OFREP.
+ */
+@Slf4j
+public final class OfrepProvider implements FeatureProvider {
+
+ private static final String OFREP_PROVIDER = "ofrep";
+ private static final long DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT = 1000;
+
+ private final Resolver ofrepResolver;
+ private Executor executor;
+
+ public static OfrepProvider constructProvider() {
+ return new OfrepProvider();
+ }
+
+ /**
+ * Constructs an OfrepProvider with the specified options.
+ *
+ * @param options The options for configuring the provider.
+ * @return An instance of OfrepProvider configured with the provided options.
+ * @throws IllegalArgumentException if any of the options are invalid.
+ */
+ public static OfrepProvider constructProvider(OfrepProviderOptions options) {
+ if (!isValidUrl(options.getBaseUrl())) {
+ throw new IllegalArgumentException("Invalid base URL: " + options.getBaseUrl());
+ }
+
+ if (options.getHeaders() == null) {
+ throw new IllegalArgumentException("Headers cannot be null");
+ }
+
+ if (options.getRequestTimeout() == null
+ || options.getRequestTimeout().isNegative()
+ || options.getRequestTimeout().isZero()) {
+ throw new IllegalArgumentException("Request timeout must be a positive duration");
+ }
+
+ if (options.getConnectTimeout() == null
+ || options.getConnectTimeout().isNegative()
+ || options.getConnectTimeout().isZero()) {
+ throw new IllegalArgumentException("Connect timeout must be a positive duration");
+ }
+
+ if (options.getProxySelector() == null) {
+ throw new IllegalArgumentException("ProxySelector cannot be null");
+ }
+
+ if (options.getExecutor() == null) {
+ throw new IllegalArgumentException("Executor cannot be null");
+ }
+
+ return new OfrepProvider(options);
+ }
+
+ private OfrepProvider() {
+ this(new OfrepProviderOptions.Builder().build());
+ }
+
+ private OfrepProvider(OfrepProviderOptions options) {
+ this.executor = options.getExecutor();
+ this.ofrepResolver = new Resolver(
+ options.getBaseUrl(),
+ options.getHeaders(),
+ options.getRequestTimeout(),
+ options.getConnectTimeout(),
+ options.getProxySelector(),
+ options.getExecutor());
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return () -> OFREP_PROVIDER;
+ }
+
+ @Override
+ public void shutdown() {
+ if (executor instanceof ExecutorService) {
+ ExecutorService executorService = (ExecutorService) executor;
+ try {
+ executorService.shutdown();
+
+ if (!executorService.awaitTermination(DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ executorService.shutdownNow();
+ if (!executorService.awaitTermination(DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ log.error("Provider couldn't shutdown gracefully.");
+ }
+ }
+ } catch (InterruptedException e) {
+ executorService.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return ofrepResolver.resolveBoolean(key, defaultValue, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
+ return ofrepResolver.resolveString(key, defaultValue, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
+ return ofrepResolver.resolveInteger(key, defaultValue, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
+ return ofrepResolver.resolveDouble(key, defaultValue, ctx);
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
+ return ofrepResolver.resolveObject(key, defaultValue, ctx);
+ }
+
+ private static boolean isValidUrl(String url) {
+ UrlValidator validator = new UrlValidator(new String[] {"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS);
+ return validator.isValid(url);
+ }
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProviderOptions.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProviderOptions.java
new file mode 100644
index 000000000..abdff58bc
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/OfrepProviderOptions.java
@@ -0,0 +1,38 @@
+package dev.openfeature.contrib.providers.ofrep;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.net.ProxySelector;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * Options for configuring the OFREP provider.
+ */
+@Getter
+@Builder(builderClassName = "Builder", buildMethodName = "build")
+public class OfrepProviderOptions {
+
+ private static final int DEFAULT_THREAD_POOL_SIZE = 5;
+
+ @Builder.Default
+ private final String baseUrl = "http://localhost:8016";
+
+ @Builder.Default
+ private final ProxySelector proxySelector = ProxySelector.getDefault();
+
+ @Builder.Default
+ private final Executor executor = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE);
+
+ @Builder.Default
+ private final Duration requestTimeout = Duration.ofSeconds(10);
+
+ @Builder.Default
+ private final Duration connectTimeout = Duration.ofSeconds(10);
+
+ @Builder.Default
+ private final ImmutableMap> headers = ImmutableMap.of();
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepApi.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepApi.java
new file mode 100644
index 000000000..1c7025155
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepApi.java
@@ -0,0 +1,132 @@
+package dev.openfeature.contrib.providers.ofrep.internal;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import dev.openfeature.sdk.exceptions.GeneralError;
+import dev.openfeature.sdk.exceptions.ParseError;
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.Executor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The OfrepApi class is responsible for communicating with the OFREP server to evaluate flags.
+ */
+@Slf4j
+public class OfrepApi {
+
+ private static final String path = "/ofrep/v1/evaluate/flags/";
+ private final ObjectMapper serializer;
+ private final ObjectMapper deserializer;
+ private final HttpClient httpClient;
+ private final Duration requestTimeout;
+ private Instant nextAllowedRequestTime = Instant.now();
+
+ /**
+ * Constructs an OfrepApi instance with a HTTP client and JSON serializers.
+ *
+ * @param requestTimeout - The request timeout duration for the request.
+ * @param connectTimeout - The connect timeout duration for establishing HTTP connection.
+ * @param proxySelector - The ProxySelector to use for HTTP requests.
+ * @param executor - The Executor to use for operations.
+ */
+ public OfrepApi(Duration requestTimeout, Duration connectTimeout, ProxySelector proxySelector, Executor executor) {
+ httpClient = HttpClient.newBuilder()
+ .connectTimeout(connectTimeout)
+ .proxy(proxySelector)
+ .executor(executor)
+ .build();
+ this.requestTimeout = requestTimeout;
+ serializer = new ObjectMapper();
+ deserializer = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ /**
+ * prepareHttpRequest is preparing the request to be sent to the OFREP Server.
+ *
+ * @param url - url of the request
+ * @param headers - headers to be included in the request
+ * @param requestBody - body of the request
+ *
+ * @return HttpRequest ready to be sent
+ * @throws JsonProcessingException - if an error occurred while processing the json.
+ */
+ private HttpRequest prepareHttpRequest(
+ final URI url, ImmutableMap> headers, final T requestBody)
+ throws JsonProcessingException {
+
+ HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
+ .uri(url)
+ .timeout(this.requestTimeout)
+ .header("Content-Type", "application/json; charset=utf-8")
+ .POST(HttpRequest.BodyPublishers.ofByteArray(serializer.writeValueAsBytes(requestBody)));
+
+ for (ImmutableMap.Entry> entry : headers.entrySet()) {
+ String key = entry.getKey();
+ for (String value : entry.getValue()) {
+ reqBuilder.header(key, value);
+ }
+ }
+
+ return reqBuilder.build();
+ }
+
+ /**
+ * resolve is the method that interacts with the OFREP server to evaluate flags.
+ *
+ * @param baseUrl - The base URL of the OFREP server.
+ * @param headers - headers to include in the request.
+ * @param key - The flag key to evaluate.
+ * @param requestBody - The evaluation context as a map of key-value pairs.
+ *
+ * @return Resolution object, containing the response status, headers, and body.
+ */
+ public Resolution resolve(
+ String baseUrl,
+ ImmutableMap> headers,
+ String key,
+ final OfrepRequest requestBody) {
+ if (nextAllowedRequestTime.isAfter(Instant.now())) {
+ throw new GeneralError("Rate limit exceeded. Please wait before making another request.");
+ }
+
+ try {
+ String fullPath = baseUrl + path + key;
+ URI uri = URI.create(fullPath);
+
+ HttpRequest request = prepareHttpRequest(uri, headers, requestBody);
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ OfrepResponse responseBody = deserializer.readValue(response.body(), OfrepResponse.class);
+
+ return new Resolution(response.statusCode(), response.headers(), responseBody);
+ } catch (JsonProcessingException e) {
+ throw new ParseError("Error processing JSON: " + e.getMessage());
+ } catch (IOException e) {
+ throw new GeneralError("IO error: " + e.getMessage());
+ } catch (InterruptedException e) {
+ throw new GeneralError("Request interrupted: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Sets the next allowed request time based on the Retry-After header.
+ * If the provided time is later than the current next allowed request time, it updates it.
+ *
+ * @param retryAfter The value of the Retry-After header, which can be a number of seconds or a date string.
+ */
+ public void setNextAllowedRequestTime(Instant retryAfter) {
+ if (retryAfter.isAfter(nextAllowedRequestTime)) {
+ nextAllowedRequestTime = retryAfter;
+ }
+ }
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepRequest.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepRequest.java
new file mode 100644
index 000000000..189de0ae1
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepRequest.java
@@ -0,0 +1,12 @@
+package dev.openfeature.contrib.providers.ofrep.internal;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.Value;
+
+/**
+ * Represents the request body for the OFREP API request.
+ */
+@Value
+public class OfrepRequest {
+ ImmutableMap context;
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepResponse.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepResponse.java
new file mode 100644
index 000000000..e8d80f6d7
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/OfrepResponse.java
@@ -0,0 +1,50 @@
+package dev.openfeature.contrib.providers.ofrep.internal;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * This class represents the response from an OFREP response.
+ */
+@Getter
+@Setter
+@ToString
+public class OfrepResponse {
+ private Object value;
+ private String key;
+ private String variant;
+ private String reason;
+ private boolean cacheable;
+ private String errorCode;
+ private String errorDetails;
+ private Map metadata;
+
+ public Map getMetadata() {
+ return ImmutableMap.copyOf(metadata);
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata != null ? ImmutableMap.copyOf(metadata) : ImmutableMap.of();
+ }
+
+ /**
+ * Creates a copy of the current OfrepResponse instance.
+ *
+ * @return A new OfrepResponse instance with the same values as the provided instance.
+ */
+ public OfrepResponse copy() {
+ OfrepResponse newResponse = new OfrepResponse();
+ newResponse.value = this.value;
+ newResponse.key = this.key;
+ newResponse.variant = this.variant;
+ newResponse.reason = this.reason;
+ newResponse.cacheable = this.cacheable;
+ newResponse.errorCode = this.errorCode;
+ newResponse.errorDetails = this.errorDetails;
+ newResponse.metadata = metadata != null ? ImmutableMap.copyOf(metadata) : ImmutableMap.of();
+ return newResponse;
+ }
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolution.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolution.java
new file mode 100644
index 000000000..7e91c027a
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolution.java
@@ -0,0 +1,31 @@
+package dev.openfeature.contrib.providers.ofrep.internal;
+
+import java.net.http.HttpHeaders;
+import lombok.Getter;
+
+/**
+ * Resolution class encapsulates the response from the OFREP server, along with additional fields.
+ */
+@Getter
+public class Resolution {
+ private final int responseStatus;
+ private final HttpHeaders headers;
+ private final OfrepResponse response;
+
+ /**
+ * Constructs a Resolution object with the given response status, headers, and response body.
+ *
+ * @param responseStatus - The HTTP response status code.
+ * @param headers - The HTTP headers from the response.
+ * @param response - The parsed response body as an OfrepResponse object.
+ */
+ public Resolution(int responseStatus, HttpHeaders headers, OfrepResponse response) {
+ this.responseStatus = responseStatus;
+ this.headers = headers;
+ this.response = response.copy();
+ }
+
+ public OfrepResponse getResponse() {
+ return response.copy();
+ }
+}
diff --git a/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolver.java b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolver.java
new file mode 100644
index 000000000..31f282e26
--- /dev/null
+++ b/providers/ofrep/src/main/java/dev/openfeature/contrib/providers/ofrep/internal/Resolver.java
@@ -0,0 +1,248 @@
+package dev.openfeature.contrib.providers.ofrep.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import dev.openfeature.sdk.ErrorCode;
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.ImmutableMetadata;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Value;
+import dev.openfeature.sdk.exceptions.GeneralError;
+import java.net.HttpURLConnection;
+import java.net.ProxySelector;
+import java.net.http.HttpHeaders;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Resolver class that interacts with the OFREP API to resolve feature flags.
+ */
+@Slf4j
+public class Resolver {
+
+ private final String baseUrl;
+ private final ImmutableMap> headers;
+ private final OfrepApi ofrepApi;
+
+ /**
+ * Constructs a Resolver with the specified base URL, headers, timeout, proxySelector and executor.
+ *
+ * @param baseUrl - The base URL of the OFREP server.
+ * @param headers - The headers to include in the requests.
+ * @param requestTimeout - The request timeout duration for the request.
+ * @param connectTimeout - The connect timeout duration for establishing the HTTP connection.
+ * @param proxySelector - The ProxySelector to use for HTTP requests.
+ * @param executor - The Executor to use for operations.
+ */
+ public Resolver(
+ String baseUrl,
+ ImmutableMap> headers,
+ Duration requestTimeout,
+ Duration connectTimeout,
+ ProxySelector proxySelector,
+ Executor executor) {
+ this.baseUrl = baseUrl;
+ this.headers = headers;
+ this.ofrepApi = new OfrepApi(requestTimeout, connectTimeout, proxySelector, executor);
+ }
+
+ private ProviderEvaluation resolve(Class type, String key, T defaultValue, EvaluationContext ctx) {
+ try {
+ OfrepRequest ofrepRequest = new OfrepRequest(ImmutableMap.copyOf(ctx.asObjectMap()));
+ Resolution resolution = ofrepApi.resolve(this.baseUrl, this.headers, key, ofrepRequest);
+
+ int responseStatus = resolution.getResponseStatus();
+ HttpHeaders responseHeaders = resolution.getHeaders();
+ OfrepResponse responseBody = resolution.getResponse();
+
+ Map metadata = responseBody.getMetadata();
+ ImmutableMetadata immutableMetadata = convertToImmutableMetadata(metadata);
+
+ switch (responseStatus) {
+ case HttpURLConnection.HTTP_OK:
+ return handleResolved(key, defaultValue, type, responseBody, immutableMetadata);
+ case HttpURLConnection.HTTP_UNAUTHORIZED:
+ case HttpURLConnection.HTTP_FORBIDDEN:
+ return handleGeneralError(
+ defaultValue, immutableMetadata, "authentication/authorization error for flag: " + key);
+ case HttpURLConnection.HTTP_BAD_REQUEST:
+ case HttpURLConnection.HTTP_NOT_ACCEPTABLE:
+ return handleInvalidContext(key, defaultValue, immutableMetadata);
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ return handleFlagNotFound(key, defaultValue, immutableMetadata);
+ case 429:
+ String retryAfter =
+ responseHeaders.firstValue("Retry-After").orElse(null);
+ Instant retryAfterInstant = parseRetryAfter(retryAfter);
+ ofrepApi.setNextAllowedRequestTime(retryAfterInstant);
+ return handleGeneralError(
+ defaultValue,
+ immutableMetadata,
+ "Rate limit exceeded for flag: " + key + ", retry after: " + retryAfterInstant);
+ default:
+ return handleGeneralError(
+ defaultValue,
+ immutableMetadata,
+ "Unknown error while retrieving flag: " + key + ", status code: " + responseStatus);
+ }
+ } catch (GeneralError e) {
+ String errorMessage = "general error for flag: " + key + "; " + e.getMessage();
+ return handleGeneralError(defaultValue, ImmutableMetadata.builder().build(), errorMessage);
+ }
+ }
+
+ public ProviderEvaluation resolveBoolean(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return resolve(Boolean.class, key, defaultValue, ctx);
+ }
+
+ public ProviderEvaluation resolveString(String key, String defaultValue, EvaluationContext ctx) {
+ return resolve(String.class, key, defaultValue, ctx);
+ }
+
+ public ProviderEvaluation resolveInteger(String key, Integer defaultValue, EvaluationContext ctx) {
+ return resolve(Integer.class, key, defaultValue, ctx);
+ }
+
+ public ProviderEvaluation resolveDouble(String key, Double defaultValue, EvaluationContext ctx) {
+ return resolve(Double.class, key, defaultValue, ctx);
+ }
+
+ /**
+ * Resolves an object value for the given key.
+ *
+ * @param key - The flag key to evaluate.
+ * @param defaultValue - The default value to return if there is an error.
+ * @param ctx - The evaluation context containing additional information.
+ *
+ * @return A ProviderEvaluation containing the resolved value, variant,
+ * reason, and metadata.
+ */
+ public ProviderEvaluation resolveObject(String key, Value defaultValue, EvaluationContext ctx) {
+ ProviderEvaluation