diff --git a/CHANGES.txt b/CHANGES.txt
index 072fab1f9..9c10df67f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,6 @@
+4.13.0 (Sep 13, 2024)
+- Added support for Kerberos Proxy authentication.
+
4.12.1 (Jun 10, 2024)
- Fixed deadlock for virtual thread in Push Manager and SSE Client.
diff --git a/client/pom.xml b/client/pom.xml
index b8d94bba7..2c1892ca2 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -5,7 +5,7 @@
io.split.client
java-client-parent
- 4.12.1
+ 4.13.0
java-client
jar
@@ -64,6 +64,7 @@
io.split.schemas:*
io.codigo.grammar:*
org.apache.httpcomponents.*
+ org.apache.hc.*
com.google.*
org.yaml:snakeyaml:*
@@ -238,5 +239,17 @@
4.0.3
test
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
+ test
+
diff --git a/client/src/main/java/io/split/client/RequestDecorator.java b/client/src/main/java/io/split/client/RequestDecorator.java
index 1572463ef..33059e617 100644
--- a/client/src/main/java/io/split/client/RequestDecorator.java
+++ b/client/src/main/java/io/split/client/RequestDecorator.java
@@ -2,16 +2,12 @@
import io.split.client.dtos.RequestContext;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.Header;
import java.util.HashSet;
-import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
-import java.util.ArrayList;
import java.util.Set;
-import java.util.List;
+import java.util.stream.Collectors;
public final class RequestDecorator {
CustomHeaderDecorator _headerDecorator;
@@ -36,42 +32,16 @@ public RequestDecorator(CustomHeaderDecorator headerDecorator) {
: headerDecorator;
}
- public HttpRequest decorateHeaders(HttpRequest request) {
+ public RequestContext decorateHeaders(RequestContext request) {
try {
- Map> headers = _headerDecorator
- .getHeaderOverrides(new RequestContext(convertToMap(request.getHeaders())));
- for (Map.Entry> entry : headers.entrySet()) {
- if (isHeaderAllowed(entry.getKey())) {
- List values = entry.getValue();
- for (int i = 0; i < values.size(); i++) {
- if (i == 0) {
- request.setHeader(entry.getKey(), values.get(i));
- } else {
- request.addHeader(entry.getKey(), values.get(i));
- }
- }
- }
- }
+ return new RequestContext(_headerDecorator.getHeaderOverrides(request)
+ .entrySet()
+ .stream()
+ .filter(e -> !forbiddenHeaders.contains(e.getKey().toLowerCase()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("Problem adding custom headers to request decorator: %s", e), e);
}
-
- return request;
- }
-
- private boolean isHeaderAllowed(String headerName) {
- return !forbiddenHeaders.contains(headerName.toLowerCase());
- }
-
- private Map> convertToMap(Header[] to_convert) {
- Map> to_return = new HashMap>();
- for (Integer i = 0; i < to_convert.length; i++) {
- if (!to_return.containsKey(to_convert[i].getName())) {
- to_return.put(to_convert[i].getName(), new ArrayList());
- }
- to_return.get(to_convert[i].getName()).add(to_convert[i].getValue());
- }
- return to_return;
}
}
diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java
index 2f29c1719..8787c1069 100644
--- a/client/src/main/java/io/split/client/SplitClientConfig.java
+++ b/client/src/main/java/io/split/client/SplitClientConfig.java
@@ -4,6 +4,7 @@
import io.split.client.impressions.ImpressionsManager;
import io.split.client.utils.FileTypeEnum;
import io.split.integrations.IntegrationsConfig;
+import io.split.service.CustomHttpModule;
import io.split.storages.enums.OperationMode;
import io.split.storages.enums.StorageMode;
import org.apache.hc.core5.http.HttpHost;
@@ -91,7 +92,7 @@ public class SplitClientConfig {
private final HashSet _flagSetsFilter;
private final int _invalidSets;
private final CustomHeaderDecorator _customHeaderDecorator;
-
+ private final CustomHttpModule _alternativeHTTPModule;
public static Builder builder() {
return new Builder();
@@ -148,7 +149,8 @@ private SplitClientConfig(String endpoint,
ThreadFactory threadFactory,
HashSet flagSetsFilter,
int invalidSets,
- CustomHeaderDecorator customHeaderDecorator) {
+ CustomHeaderDecorator customHeaderDecorator,
+ CustomHttpModule alternativeHTTPModule) {
_endpoint = endpoint;
_eventsEndpoint = eventsEndpoint;
_featuresRefreshRate = pollForFeatureChangesEveryNSeconds;
@@ -201,6 +203,7 @@ private SplitClientConfig(String endpoint,
_flagSetsFilter = flagSetsFilter;
_invalidSets = invalidSets;
_customHeaderDecorator = customHeaderDecorator;
+ _alternativeHTTPModule = alternativeHTTPModule;
Properties props = new Properties();
try {
@@ -409,6 +412,7 @@ public CustomHeaderDecorator customHeaderDecorator() {
return _customHeaderDecorator;
}
+ public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; }
public static final class Builder {
private String _endpoint = SDK_ENDPOINT;
@@ -466,6 +470,7 @@ public static final class Builder {
private HashSet _flagSetsFilter = new HashSet<>();
private int _invalidSetsCount = 0;
private CustomHeaderDecorator _customHeaderDecorator = null;
+ private CustomHttpModule _alternativeHTTPModule = null;
public Builder() {
}
@@ -960,6 +965,17 @@ public Builder customHeaderDecorator(CustomHeaderDecorator customHeaderDecorator
return this;
}
+ /**
+ * Alternative Http Client
+ *
+ * @param alternativeHTTPModule
+ * @return this builder
+ */
+ public Builder alternativeHTTPModule(CustomHttpModule alternativeHTTPModule) {
+ _alternativeHTTPModule = alternativeHTTPModule;
+ return this;
+ }
+
/**
* Thread Factory
*
@@ -971,7 +987,7 @@ public Builder threadFactory(ThreadFactory threadFactory) {
return this;
}
- public SplitClientConfig build() {
+ private void verifyRates() {
if (_featuresRefreshRate < 5 ) {
throw new IllegalArgumentException("featuresRefreshRate must be >= 5: " + _featuresRefreshRate);
}
@@ -980,15 +996,6 @@ public SplitClientConfig build() {
throw new IllegalArgumentException("segmentsRefreshRate must be >= 30: " + _segmentsRefreshRate);
}
- switch (_impressionsMode) {
- case OPTIMIZED:
- _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 300 : Math.max(60, _impressionsRefreshRate);
- break;
- case DEBUG:
- _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 60 : _impressionsRefreshRate;
- break;
- }
-
if (_eventSendIntervalInMillis < 1000) {
throw new IllegalArgumentException("_eventSendIntervalInMillis must be >= 1000: " + _eventSendIntervalInMillis);
}
@@ -996,19 +1003,12 @@ public SplitClientConfig build() {
if (_metricsRefreshRate < 30) {
throw new IllegalArgumentException("metricsRefreshRate must be >= 30: " + _metricsRefreshRate);
}
-
- if (_impressionsQueueSize <=0 ) {
- throw new IllegalArgumentException("impressionsQueueSize must be > 0: " + _impressionsQueueSize);
- }
-
- if (_connectionTimeout <= 0) {
- throw new IllegalArgumentException("connectionTimeOutInMs must be > 0: " + _connectionTimeout);
- }
-
- if (_readTimeout <= 0) {
- throw new IllegalArgumentException("readTimeout must be > 0: " + _readTimeout);
+ if(_telemetryRefreshRate < 60) {
+ throw new IllegalStateException("_telemetryRefreshRate must be >= 60");
}
+ }
+ private void verifyEndPoints() {
if (_endpoint == null) {
throw new IllegalArgumentException("endpoint must not be null");
}
@@ -1021,18 +1021,6 @@ public SplitClientConfig build() {
throw new IllegalArgumentException("If endpoint is set, you must also set the events endpoint");
}
- if (_numThreadsForSegmentFetch <= 0) {
- throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero");
- }
-
- if (_authRetryBackoffBase <= 0) {
- throw new IllegalArgumentException("authRetryBackoffBase: must be >= 1");
- }
-
- if (_streamingReconnectBackoffBase <= 0) {
- throw new IllegalArgumentException("streamingReconnectBackoffBase: must be >= 1");
- }
-
if (_authServiceURL == null) {
throw new IllegalArgumentException("authServiceURL must not be null");
}
@@ -1044,22 +1032,26 @@ public SplitClientConfig build() {
if (_telemetryURl == null) {
throw new IllegalArgumentException("telemetryURl must not be null");
}
+ }
- if (_onDemandFetchRetryDelayMs <= 0) {
- throw new IllegalStateException("streamingRetryDelay must be > 0");
+ private void verifyAllModes() {
+ switch (_impressionsMode) {
+ case OPTIMIZED:
+ _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 300 : Math.max(60, _impressionsRefreshRate);
+ break;
+ case DEBUG:
+ _impressionsRefreshRate = (_impressionsRefreshRate <= 0) ? 60 : _impressionsRefreshRate;
+ break;
+ case NONE:
+ break;
}
- if(_onDemandFetchMaxRetries <= 0) {
- throw new IllegalStateException("_onDemandFetchMaxRetries must be > 0");
+ if (_impressionsQueueSize <=0 ) {
+ throw new IllegalArgumentException("impressionsQueueSize must be > 0: " + _impressionsQueueSize);
}
-
if(_storageMode == null) {
_storageMode = StorageMode.MEMORY;
}
-
- if(_telemetryRefreshRate < 60) {
- throw new IllegalStateException("_telemetryRefreshRate must be >= 60");
- }
if(OperationMode.CONSUMER.equals(_operationMode)){
if(_customStorageWrapper == null) {
@@ -1067,8 +1059,56 @@ public SplitClientConfig build() {
}
_storageMode = StorageMode.PLUGGABLE;
}
+ }
+
+ private void verifyNetworkParams() {
+ if (_connectionTimeout <= 0) {
+ throw new IllegalArgumentException("connectionTimeOutInMs must be > 0: " + _connectionTimeout);
+ }
+
+ if (_readTimeout <= 0) {
+ throw new IllegalArgumentException("readTimeout must be > 0: " + _readTimeout);
+ }
+ if (_authRetryBackoffBase <= 0) {
+ throw new IllegalArgumentException("authRetryBackoffBase: must be >= 1");
+ }
+
+ if (_streamingReconnectBackoffBase <= 0) {
+ throw new IllegalArgumentException("streamingReconnectBackoffBase: must be >= 1");
+ }
+
+ if (_onDemandFetchRetryDelayMs <= 0) {
+ throw new IllegalStateException("streamingRetryDelay must be > 0");
+ }
+
+ if(_onDemandFetchMaxRetries <= 0) {
+ throw new IllegalStateException("_onDemandFetchMaxRetries must be > 0");
+ }
+ }
+
+ private void verifyAlternativeClient() {
+ if (_alternativeHTTPModule != null && _streamingEnabled) {
+ throw new IllegalArgumentException("Streaming feature is not supported with Alternative HTTP Client");
+ }
+ }
+
+ public SplitClientConfig build() {
+
+ verifyRates();
+
+ verifyAllModes();
+
+ verifyEndPoints();
+
+ verifyNetworkParams();
+
+ verifyAlternativeClient();
+
+ if (_numThreadsForSegmentFetch <= 0) {
+ throw new IllegalArgumentException("Number of threads for fetching segments MUST be greater than zero");
+ }
- return new SplitClientConfig(
+ return new SplitClientConfig(
_endpoint,
_eventsEndpoint,
_featuresRefreshRate,
@@ -1120,7 +1160,8 @@ public SplitClientConfig build() {
_threadFactory,
_flagSetsFilter,
_invalidSetsCount,
- _customHeaderDecorator);
+ _customHeaderDecorator,
+ _alternativeHTTPModule);
}
}
}
\ No newline at end of file
diff --git a/client/src/main/java/io/split/client/SplitFactoryBuilder.java b/client/src/main/java/io/split/client/SplitFactoryBuilder.java
index c2271ec4f..2b48fb0d3 100644
--- a/client/src/main/java/io/split/client/SplitFactoryBuilder.java
+++ b/client/src/main/java/io/split/client/SplitFactoryBuilder.java
@@ -2,6 +2,7 @@
import io.split.inputValidation.ApiKeyValidator;
import io.split.grammar.Treatments;
+import io.split.service.SplitHttpClient;
import io.split.storages.enums.StorageMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java
index a783b1445..3102d3e17 100644
--- a/client/src/main/java/io/split/client/SplitFactoryImpl.java
+++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java
@@ -57,8 +57,9 @@
import io.split.engine.segments.SegmentChangeFetcher;
import io.split.engine.segments.SegmentSynchronizationTaskImp;
import io.split.integrations.IntegrationsConfig;
-import io.split.service.SplitHttpClient;
import io.split.service.SplitHttpClientImpl;
+import io.split.service.SplitHttpClient;
+
import io.split.storages.SegmentCache;
import io.split.storages.SegmentCacheConsumer;
import io.split.storages.SegmentCacheProducer;
@@ -83,6 +84,7 @@
import io.split.telemetry.synchronizer.TelemetryInMemorySubmitter;
import io.split.telemetry.synchronizer.TelemetrySyncTask;
import io.split.telemetry.synchronizer.TelemetrySynchronizer;
+
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
@@ -102,7 +104,6 @@
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
-import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pluggable.CustomStorageWrapper;
@@ -111,16 +112,16 @@
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
import static io.split.client.utils.SplitExecutorFactory.buildExecutorService;
public class SplitFactoryImpl implements SplitFactory {
- private static final Logger _log = LoggerFactory.getLogger(SplitFactory.class);
+ private static final org.slf4j.Logger _log = LoggerFactory.getLogger(SplitFactoryImpl.class);
private static final String LEGACY_LOG_MESSAGE = "The sdk initialize in localhost mode using Legacy file. The splitFile or "
+
"inputStream doesn't add it to the config.";
@@ -155,15 +156,16 @@ public class SplitFactoryImpl implements SplitFactory {
private final SplitSynchronizationTask _splitSynchronizationTask;
private final EventsTask _eventsTask;
private final SyncManager _syncManager;
- private final SplitHttpClient _splitHttpClient;
+ private SplitHttpClient _splitHttpClient;
private final UserStorageWrapper _userStorageWrapper;
private final ImpressionsSender _impressionsSender;
private final URI _rootTarget;
private final URI _eventsRootTarget;
private final UniqueKeysTracker _uniqueKeysTracker;
+ private RequestDecorator _requestDecorator;
// Constructor for standalone mode
- public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException {
+ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException, IOException {
_userStorageWrapper = null;
_operationMode = config.operationMode();
_startTime = System.currentTimeMillis();
@@ -186,9 +188,13 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn
// SDKReadinessGates
_gates = new SDKReadinessGates();
+ _requestDecorator = new RequestDecorator(config.customHeaderDecorator());
// HttpClient
- RequestDecorator requestDecorator = new RequestDecorator(config.customHeaderDecorator());
- _splitHttpClient = buildSplitHttpClient(apiToken, config, _sdkMetadata, requestDecorator);
+ if (config.alternativeHTTPModule() == null) {
+ _splitHttpClient = buildSplitHttpClient(apiToken, config, _sdkMetadata, _requestDecorator);
+ } else {
+ _splitHttpClient = config.alternativeHTTPModule().createClient(apiToken, _sdkMetadata, _requestDecorator);
+ }
// Roots
_rootTarget = URI.create(config.endpoint());
@@ -234,7 +240,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn
EventsSender eventsSender = EventsSender.create(_splitHttpClient, _eventsRootTarget, _telemetryStorageProducer);
_eventsTask = EventsTask.create(config.eventSendIntervalInMillis(), eventsStorage, eventsSender,
config.getThreadFactory());
- _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, config.getThreadFactory());
+ _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer,
+ config.getThreadFactory());
// Evaluator
_evaluator = new EvaluatorImp(splitCache, segmentCache);
@@ -257,7 +264,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn
// SyncManager
SplitTasks splitTasks = SplitTasks.build(_splitSynchronizationTask, _segmentSynchronizationTaskImp,
_impressionsManager, _eventsTask, _telemetrySyncTask, _uniqueKeysTracker);
- SplitAPI splitAPI = SplitAPI.build(_splitHttpClient, buildSSEdHttpClient(apiToken, config, _sdkMetadata), requestDecorator);
+ SplitAPI splitAPI = SplitAPI.build(_splitHttpClient, buildSSEdHttpClient(apiToken, config, _sdkMetadata),
+ _requestDecorator);
_syncManager = SyncManagerImp.build(splitTasks, _splitFetcher, splitCache, splitAPI,
segmentCache, _gates, _telemetryStorageProducer, _telemetrySynchronizer, config, splitParser,
@@ -328,8 +336,10 @@ protected SplitFactoryImpl(String apiToken, SplitClientConfig config, CustomStor
_evaluator = new EvaluatorImp(userCustomSplitAdapterConsumer, userCustomSegmentAdapterConsumer);
_impressionsSender = PluggableImpressionSender.create(customStorageWrapper);
_uniqueKeysTracker = createUniqueKeysTracker(config);
- _impressionsManager = buildImpressionsManager(config, userCustomImpressionAdapterConsumer, userCustomImpressionAdapterProducer);
- _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer, config.getThreadFactory());
+ _impressionsManager = buildImpressionsManager(config, userCustomImpressionAdapterConsumer,
+ userCustomImpressionAdapterProducer);
+ _telemetrySyncTask = new TelemetrySyncTask(config.getTelemetryRefreshRate(), _telemetrySynchronizer,
+ config.getThreadFactory());
SplitTasks splitTasks = SplitTasks.build(null, null,
_impressionsManager, null, _telemetrySyncTask, _uniqueKeysTracker);
@@ -491,7 +501,7 @@ public boolean isDestroyed() {
return isTerminated;
}
- private static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config,
+ protected static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClientConfig config,
SDKMetadata sdkMetadata, RequestDecorator requestDecorator)
throws URISyntaxException {
SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create()
diff --git a/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java
index a5474cf5b..259ed0794 100644
--- a/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java
+++ b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java
@@ -1,7 +1,7 @@
package io.split.client.dtos;
-import java.util.Map;
-import org.apache.hc.core5.http.Header;
+import java.util.List;
+
/**
* A structure for returning http call results information
*/
@@ -11,15 +11,42 @@ public class SplitHttpResponse {
private final String _body;
private final Header[] _responseHeaders;
+ public static class Header {
+ private String _name;
+ private List _values;
+
+ public Header(String name, List values) {
+ _name = name;
+ _values = values;
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public List getValues() {
+ return _values;
+ }
+ };
+
public SplitHttpResponse(Integer statusCode, String statusMessage, String body, Header[] headers) {
_statusCode = statusCode;
_statusMessage = statusMessage;
_body = body;
_responseHeaders = headers;
}
+
+ public SplitHttpResponse(Integer statusCode, String statusMessage, String body, List headers) {
+ _statusCode = statusCode;
+ _statusMessage = statusMessage;
+ _body = body;
+ _responseHeaders = headers.toArray(new Header[0]);
+ }
+
public Integer statusCode() {
return _statusCode;
}
+
public String statusMessage() {
return _statusMessage;
}
diff --git a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java
index 06df64cc4..35c0f57f2 100644
--- a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java
+++ b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java
@@ -4,6 +4,7 @@
import io.split.client.dtos.ImpressionCount;
import io.split.client.dtos.SplitHttpResponse;
import io.split.client.dtos.TestImpressions;
+import io.split.client.utils.Json;
import io.split.client.utils.Utils;
import io.split.service.SplitHttpClient;
@@ -11,7 +12,6 @@
import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum;
import io.split.telemetry.domain.enums.ResourceEnum;
import io.split.telemetry.storage.TelemetryRuntimeProducer;
-import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -67,10 +67,12 @@ private HttpImpressionsSender(SplitHttpClient client, URI impressionBulkTarget,
public void postImpressionsBulk(List impressions) {
long initTime = System.currentTimeMillis();
try {
- HttpEntity entity = Utils.toJsonEntity(impressions);
- Map> additionalHeaders = Collections.singletonMap(IMPRESSIONS_MODE_HEADER,
- Collections.singletonList(_mode.toString()));
- SplitHttpResponse response = _client.post(_impressionBulkTarget, entity, additionalHeaders);
+ Map> additionalHeaders = new HashMap<>();
+ additionalHeaders.put(IMPRESSIONS_MODE_HEADER, Collections.singletonList(_mode.toString()));
+ additionalHeaders.put("Content-Type", Collections.singletonList("application/json"));
+
+ SplitHttpResponse response = _client.post(_impressionBulkTarget, Json.toJson(impressions),
+ additionalHeaders);
if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
_telemetryRuntimeProducer.recordSyncError(ResourceEnum.IMPRESSION_SYNC, response.statusCode());
@@ -95,8 +97,12 @@ public void postCounters(HashMap raw) {
}
try {
+
+ Map> additionalHeaders = new HashMap<>();
+ additionalHeaders.put("Content-Type", Collections.singletonList("application/json"));
+
SplitHttpResponse response = _client.post(_impressionCountTarget,
- Utils.toJsonEntity(ImpressionCount.fromImpressionCounterData(raw)),
+ Json.toJson(ImpressionCount.fromImpressionCounterData(raw)),
null);
if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
diff --git a/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java b/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java
new file mode 100644
index 000000000..c64d9d46c
--- /dev/null
+++ b/client/src/main/java/io/split/client/utils/ApacheRequestDecorator.java
@@ -0,0 +1,43 @@
+package io.split.client.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpRequest;
+
+import io.split.client.RequestDecorator;
+import io.split.client.dtos.RequestContext;
+
+public class ApacheRequestDecorator {
+
+ public static HttpRequest decorate(HttpRequest request, RequestDecorator decorator) {
+
+ RequestContext ctx = new RequestContext(convertToMap(request.getHeaders()));
+ for (Map.Entry> entry : decorator.decorateHeaders(ctx).headers().entrySet()) {
+ List values = entry.getValue();
+ for (int i = 0; i < values.size(); i++) {
+ if (i == 0) {
+ request.setHeader(entry.getKey(), values.get(i));
+ } else {
+ request.addHeader(entry.getKey(), values.get(i));
+ }
+ }
+ }
+
+ return request;
+ }
+
+ private static Map> convertToMap(Header[] to_convert) {
+ Map> to_return = new HashMap>();
+ for (Integer i = 0; i < to_convert.length; i++) {
+ if (!to_return.containsKey(to_convert[i].getName())) {
+ to_return.put(to_convert[i].getName(), new ArrayList());
+ }
+ to_return.get(to_convert[i].getName()).add(to_convert[i].getValue());
+ }
+ return to_return;
+ }
+}
diff --git a/client/src/main/java/io/split/engine/common/PushManagerImp.java b/client/src/main/java/io/split/engine/common/PushManagerImp.java
index 3c15481fd..653249308 100644
--- a/client/src/main/java/io/split/engine/common/PushManagerImp.java
+++ b/client/src/main/java/io/split/engine/common/PushManagerImp.java
@@ -84,6 +84,7 @@ public static PushManagerImp build(Synchronizer synchronizer,
telemetryRuntimeProducer, flagSetsFilter);
Worker segmentWorker = new SegmentsWorkerImp(synchronizer);
PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages, telemetryRuntimeProducer);
+
return new PushManagerImp(new AuthApiClientImp(authUrl, splitAPI.getHttpClient(), telemetryRuntimeProducer),
EventSourceClientImp.build(streamingUrl, featureFlagsWorker, segmentWorker, pushStatusTracker, splitAPI.getSseHttpClient(),
telemetryRuntimeProducer, threadFactory, splitAPI.getRequestDecorator()),
diff --git a/client/src/main/java/io/split/engine/sse/client/SSEClient.java b/client/src/main/java/io/split/engine/sse/client/SSEClient.java
index 37cc6dac9..aac6f5566 100644
--- a/client/src/main/java/io/split/engine/sse/client/SSEClient.java
+++ b/client/src/main/java/io/split/engine/sse/client/SSEClient.java
@@ -2,6 +2,7 @@
import com.google.common.base.Strings;
import io.split.client.RequestDecorator;
+import io.split.client.utils.ApacheRequestDecorator;
import io.split.telemetry.domain.StreamingEvent;
import io.split.telemetry.domain.enums.StreamEventsEnum;
import io.split.telemetry.storage.TelemetryRuntimeProducer;
@@ -64,11 +65,11 @@ private enum ConnectionState {
private final TelemetryRuntimeProducer _telemetryRuntimeProducer;
public SSEClient(Function eventCallback,
- Function statusCallback,
- CloseableHttpClient client,
- TelemetryRuntimeProducer telemetryRuntimeProducer,
- ThreadFactory threadFactory,
- RequestDecorator requestDecorator) {
+ Function statusCallback,
+ CloseableHttpClient client,
+ TelemetryRuntimeProducer telemetryRuntimeProducer,
+ ThreadFactory threadFactory,
+ RequestDecorator requestDecorator) {
_eventCallback = eventCallback;
_statusCallback = statusCallback;
_client = client;
@@ -96,7 +97,7 @@ public boolean open(URI uri) {
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- if(e.getMessage() == null){
+ if (e.getMessage() == null) {
_log.info("The thread was interrupted while opening SSEClient");
return false;
}
@@ -152,31 +153,41 @@ private void connectAndLoop(URI uri, CountDownLatch signal) {
_log.debug(exc.getMessage());
if (SOCKET_CLOSED_MESSAGE.equals(exc.getMessage())) { // Connection closed by us
_statusCallback.apply(StatusMessage.FORCED_STOP);
- _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
- StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis()));
+ _telemetryRuntimeProducer.recordStreamingEvents(
+ new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
+ StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(),
+ System.currentTimeMillis()));
return;
}
// Connection closed by server
_statusCallback.apply(StatusMessage.RETRYABLE_ERROR);
- _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
- StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis()));
+ _telemetryRuntimeProducer
+ .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
+ StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(),
+ System.currentTimeMillis()));
return;
} catch (IOException exc) { // Other type of connection error
- if(!_forcedStop.get()) {
+ if (!_forcedStop.get()) {
_log.debug(String.format("SSE connection ended abruptly: %s. Retying", exc.getMessage()));
- _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
- StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis()));
+ _telemetryRuntimeProducer.recordStreamingEvents(
+ new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
+ StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(),
+ System.currentTimeMillis()));
_statusCallback.apply(StatusMessage.RETRYABLE_ERROR);
return;
}
- _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
- StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis()));
+ _telemetryRuntimeProducer
+ .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
+ StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(),
+ System.currentTimeMillis()));
}
}
} catch (Exception e) { // Any other error non related to the connection disables streaming altogether
- _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
- StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis()));
+ _telemetryRuntimeProducer
+ .recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(),
+ StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(),
+ System.currentTimeMillis()));
_log.warn(e.getMessage(), e);
_statusCallback.apply(StatusMessage.NONRETRYABLE_ERROR);
} finally {
@@ -194,12 +205,13 @@ private void connectAndLoop(URI uri, CountDownLatch signal) {
private boolean establishConnection(URI uri, CountDownLatch signal) {
HttpGet request = new HttpGet(uri);
- request = (HttpGet) _requestDecorator.decorateHeaders(request);
+ request = (HttpGet) ApacheRequestDecorator.decorate(request, _requestDecorator);
_ongoingRequest.set(request);
try {
_ongoingResponse.set(_client.execute(_ongoingRequest.get()));
if (_ongoingResponse.get().getCode() != 200) {
- _log.error(String.format("Establishing connection, code error: %s. The url is %s", _ongoingResponse.get().getCode(), uri.toURL()));
+ _log.error(String.format("Establishing connection, code error: %s. The url is %s",
+ _ongoingResponse.get().getCode(), uri.toURL()));
return false;
}
_state.set(ConnectionState.OPEN);
@@ -236,4 +248,4 @@ private void handleMessage(String message) {
RawEvent e = RawEvent.fromString(message);
_eventCallback.apply(e);
}
-}
\ No newline at end of file
+}
diff --git a/client/src/main/java/io/split/service/CustomHttpModule.java b/client/src/main/java/io/split/service/CustomHttpModule.java
new file mode 100644
index 000000000..001648fb3
--- /dev/null
+++ b/client/src/main/java/io/split/service/CustomHttpModule.java
@@ -0,0 +1,11 @@
+package io.split.service;
+
+import io.split.client.RequestDecorator;
+import io.split.client.utils.SDKMetadata;
+
+import java.io.IOException;
+
+public interface CustomHttpModule {
+ public SplitHttpClient createClient(String apiToken, SDKMetadata sdkMetadata, RequestDecorator decorator)
+ throws IOException;
+}
diff --git a/client/src/main/java/io/split/service/HttpPostImp.java b/client/src/main/java/io/split/service/HttpPostImp.java
index e5baa001b..b33bf2103 100644
--- a/client/src/main/java/io/split/service/HttpPostImp.java
+++ b/client/src/main/java/io/split/service/HttpPostImp.java
@@ -1,15 +1,18 @@
package io.split.service;
import io.split.client.dtos.SplitHttpResponse;
-import io.split.client.utils.Utils;
+import io.split.client.utils.Json;
import io.split.telemetry.domain.enums.HttpParamsWrapper;
import io.split.telemetry.storage.TelemetryRuntimeProducer;
-import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull;
@@ -25,16 +28,19 @@ public HttpPostImp(SplitHttpClient client, TelemetryRuntimeProducer telemetryRun
public void post(URI uri, Object object, String posted, HttpParamsWrapper httpParamsWrapper) {
long initTime = System.currentTimeMillis();
- HttpEntity entity = Utils.toJsonEntity(object);
try {
- SplitHttpResponse response = _client.post(uri, entity, null);
+ Map> headers = new HashMap<>();
+ headers.put("Content-Type", Collections.singletonList("application/json"));
+ SplitHttpResponse response = _client.post(uri, Json.toJson(object), headers);
if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
_telemetryRuntimeProducer.recordSyncError(httpParamsWrapper.getResourceEnum(), response.statusCode());
return;
}
- _telemetryRuntimeProducer.recordSyncLatency(httpParamsWrapper.getHttpLatenciesEnum(), System.currentTimeMillis() - initTime);
- _telemetryRuntimeProducer.recordSuccessfulSync(httpParamsWrapper.getLastSynchronizationRecordsEnum(), System.currentTimeMillis());
+ _telemetryRuntimeProducer.recordSyncLatency(httpParamsWrapper.getHttpLatenciesEnum(),
+ System.currentTimeMillis() - initTime);
+ _telemetryRuntimeProducer.recordSuccessfulSync(httpParamsWrapper.getLastSynchronizationRecordsEnum(),
+ System.currentTimeMillis());
} catch (Throwable t) {
_logger.warn("Exception when posting " + posted + object, t);
}
diff --git a/client/src/main/java/io/split/service/SplitHttpClient.java b/client/src/main/java/io/split/service/SplitHttpClient.java
index 1c88bcd4e..899fcf56b 100644
--- a/client/src/main/java/io/split/service/SplitHttpClient.java
+++ b/client/src/main/java/io/split/service/SplitHttpClient.java
@@ -3,8 +3,6 @@
import io.split.engine.common.FetchOptions;
import io.split.client.dtos.SplitHttpResponse;
-import org.apache.hc.core5.http.HttpEntity;
-
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
@@ -30,6 +28,6 @@ public interface SplitHttpClient extends Closeable {
* @return The response structure SplitHttpResponse
*/
public SplitHttpResponse post(URI uri,
- HttpEntity entity,
+ String entity,
Map> additionalHeaders) throws IOException;
}
diff --git a/client/src/main/java/io/split/service/SplitHttpClientImpl.java b/client/src/main/java/io/split/service/SplitHttpClientImpl.java
index 64ca3a55c..7f0674411 100644
--- a/client/src/main/java/io/split/service/SplitHttpClientImpl.java
+++ b/client/src/main/java/io/split/service/SplitHttpClientImpl.java
@@ -1,6 +1,7 @@
package io.split.service;
import io.split.client.RequestDecorator;
+import io.split.client.utils.ApacheRequestDecorator;
import io.split.client.utils.SDKMetadata;
import io.split.client.utils.Utils;
import io.split.engine.common.FetchOptions;
@@ -9,9 +10,10 @@
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
-import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.HttpEntities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
@@ -19,8 +21,11 @@
import java.net.URISyntaxException;
import org.apache.hc.core5.http.HttpRequest;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
public final class SplitHttpClientImpl implements SplitHttpClient {
@@ -72,7 +77,7 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map new SplitHttpResponse.Header(h.getName(), Collections.singletonList(h.getValue())))
+ .collect(Collectors.toList()));
+ // response.getHeaders());
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e);
} finally {
@@ -98,7 +107,7 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map> additionalHeaders)
+ public SplitHttpResponse post(URI uri, String body, Map> additionalHeaders)
throws IOException {
CloseableHttpResponse response = null;
@@ -112,8 +121,8 @@ public SplitHttpResponse post(URI uri, HttpEntity entity, Map new SplitHttpResponse.Header(h.getName(), Collections.singletonList(h.getValue())))
+ .collect(Collectors.toList()));
} catch (Exception e) {
throw new IOException(String.format("Problem in http post operation: %s", e), e);
} finally {
diff --git a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java
index abcc551fe..0a154f7d4 100644
--- a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java
+++ b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java
@@ -2,6 +2,7 @@
import io.split.client.utils.LocalhostUtils;
import io.split.grammar.Treatments;
+import io.split.service.SplitHttpClient;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java
index 2d548f9e6..f2f7e3efc 100644
--- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java
+++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java
@@ -11,10 +11,11 @@
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
+import static org.mockito.Mockito.when;
import pluggable.CustomStorageWrapper;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -22,6 +23,7 @@
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
+
public class SplitFactoryImplTest extends TestCase {
public static final String API_KEY ="29013ionasdasd09u";
public static final String ENDPOINT = "https://sdk.split-stage.io";
@@ -135,7 +137,7 @@ public void testFactoryConsumerInstantiation() throws Exception {
CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class);
UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class);
TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class);
- Mockito.when(userStorageWrapper.connect()).thenReturn(true);
+ when(userStorageWrapper.connect()).thenReturn(true);
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.enableDebug()
@@ -173,7 +175,7 @@ public void testFactoryConsumerInstantiation() throws Exception {
public void testFactoryConsumerInstantiationRetryReadiness() throws Exception {
CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class);
UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class);
- Mockito.when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true);
+ when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true);
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.enableDebug()
.impressionsMode(ImpressionsManager.Mode.DEBUG)
@@ -202,7 +204,7 @@ public void testFactoryConsumerInstantiationRetryReadiness() throws Exception {
public void testFactoryConsumerDestroy() throws NoSuchFieldException, URISyntaxException, IllegalAccessException {
CustomStorageWrapper customStorageWrapper = Mockito.mock(CustomStorageWrapper.class);
UserStorageWrapper userStorageWrapper = Mockito.mock(UserStorageWrapper.class);
- Mockito.when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true);
+ when(userStorageWrapper.connect()).thenReturn(false).thenReturn(true);
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.enableDebug()
.impressionsMode(ImpressionsManager.Mode.DEBUG)
@@ -228,7 +230,7 @@ public void testFactoryConsumerDestroy() throws NoSuchFieldException, URISyntaxE
}
@Test
- public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.build();
@@ -241,7 +243,7 @@ public void testLocalhostLegacy() throws URISyntaxException, NoSuchMethodExcepti
}
@Test
- public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile("src/test/resources/split.yaml")
.setBlockUntilReadyTimeout(10000)
@@ -255,7 +257,7 @@ public void testLocalhostYaml() throws URISyntaxException, NoSuchMethodException
}
@Test
- public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile("src/test/resources/split_init.json")
.setBlockUntilReadyTimeout(10000)
@@ -270,7 +272,7 @@ public void testLocalhosJson() throws URISyntaxException, NoSuchMethodException,
@Test
public void testLocalhostYamlInputStream() throws URISyntaxException, NoSuchMethodException, InvocationTargetException,
- IllegalAccessException, FileNotFoundException {
+ IllegalAccessException, IOException {
InputStream inputStream = new FileInputStream("src/test/resources/split.yaml");
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile(inputStream, FileTypeEnum.YAML)
@@ -286,7 +288,7 @@ public void testLocalhostYamlInputStream() throws URISyntaxException, NoSuchMeth
@Test
public void testLocalhosJsonInputStream() throws URISyntaxException, NoSuchMethodException, InvocationTargetException,
- IllegalAccessException, FileNotFoundException {
+ IllegalAccessException, IOException {
InputStream inputStream = new FileInputStream("src/test/resources/split_init.json");
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile(inputStream, FileTypeEnum.JSON)
@@ -301,7 +303,7 @@ public void testLocalhosJsonInputStream() throws URISyntaxException, NoSuchMetho
}
@Test
- public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile(null, FileTypeEnum.JSON)
.setBlockUntilReadyTimeout(10000)
@@ -316,7 +318,7 @@ public void testLocalhosJsonInputStreamNull() throws URISyntaxException, NoSuchM
@Test
public void testLocalhosJsonInputStreamAndFileTypeNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException,
- IllegalAccessException, FileNotFoundException {
+ IllegalAccessException, IOException {
InputStream inputStream = new FileInputStream("src/test/resources/split_init.json");
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile(inputStream, null)
@@ -332,7 +334,7 @@ public void testLocalhosJsonInputStreamAndFileTypeNull() throws URISyntaxExcepti
@Test
public void testLocalhosJsonInputStreamNullAndFileTypeNull() throws URISyntaxException, NoSuchMethodException, InvocationTargetException,
- IllegalAccessException {
+ IllegalAccessException, IOException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.splitFile(null, null)
.setBlockUntilReadyTimeout(10000)
diff --git a/client/src/test/java/io/split/client/RequestDecoratorTest.java b/client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java
similarity index 80%
rename from client/src/test/java/io/split/client/RequestDecoratorTest.java
rename to client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java
index 62868eb40..5d5971bb8 100644
--- a/client/src/test/java/io/split/client/RequestDecoratorTest.java
+++ b/client/src/test/java/io/split/client/utils/ApacheRequestDecoratorTest.java
@@ -1,32 +1,31 @@
-package io.split.client;
+package io.split.client.utils;
+import io.split.client.CustomHeaderDecorator;
+import io.split.client.RequestDecorator;
+import io.split.client.dtos.RequestContext;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.ProtocolException;
import org.junit.Assert;
import org.junit.Test;
-import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-
-import io.split.client.dtos.RequestContext;
import java.util.List;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.Map;
-public class RequestDecoratorTest {
+public class ApacheRequestDecoratorTest {
@Test
public void testNoOp() {
- RequestDecorator decorator = new RequestDecorator(null);
+ ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator();
+ RequestDecorator requestDecorator = new RequestDecorator(null);
HttpGet request = new HttpGet("http://anyhost");
- request = (HttpGet) decorator.decorateHeaders(request);
+
+ request = (HttpGet) apacheRequestDecorator.decorate(request, requestDecorator);
Assert.assertEquals(0, request.getHeaders().length);
request.addHeader("myheader", "value");
- request = (HttpGet) decorator.decorateHeaders(request);
+ request = (HttpGet) apacheRequestDecorator.decorate(request, requestDecorator);
Assert.assertEquals(1, request.getHeaders().length);
}
@@ -45,9 +44,11 @@ public Map> getHeaderOverrides(RequestContext context) {
}
MyCustomHeaders myHeaders = new MyCustomHeaders();
RequestDecorator decorator = new RequestDecorator(myHeaders);
+ ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator();
+
HttpGet request = new HttpGet("http://anyhost");
request.addHeader("first", "myfirstheader");
- request = (HttpGet) decorator.decorateHeaders(request);
+ request = (HttpGet) apacheRequestDecorator.decorate(request, decorator);
Assert.assertEquals(4, request.getHeaders().length);
Assert.assertEquals("1", request.getHeader("first").getValue());
@@ -59,7 +60,7 @@ public Map> getHeaderOverrides(RequestContext context) {
HttpPost request2 = new HttpPost("http://anyhost");
request2.addHeader("myheader", "value");
- request2 = (HttpPost) decorator.decorateHeaders(request2);
+ request2 = (HttpPost) apacheRequestDecorator.decorate(request2, decorator);
Assert.assertEquals(5, request2.getHeaders().length);
}
@@ -88,8 +89,9 @@ public Map> getHeaderOverrides(RequestContext context) {
}
MyCustomHeaders myHeaders = new MyCustomHeaders();
RequestDecorator decorator = new RequestDecorator(myHeaders);
+ ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator();
HttpGet request = new HttpGet("http://anyhost");
- request = (HttpGet) decorator.decorateHeaders(request);
+ request = (HttpGet) apacheRequestDecorator.decorate(request, decorator);
Assert.assertEquals(1, request.getHeaders().length);
Assert.assertEquals(null, request.getHeader("SplitSDKVersion"));
}
@@ -105,7 +107,8 @@ public Map> getHeaderOverrides(RequestContext context) {
}
MyCustomHeaders myHeaders = new MyCustomHeaders();
RequestDecorator decorator = new RequestDecorator(myHeaders);
+ ApacheRequestDecorator apacheRequestDecorator = new ApacheRequestDecorator();
HttpGet request = new HttpGet("http://anyhost");
- request = (HttpGet) decorator.decorateHeaders(request);
+ request = (HttpGet) apacheRequestDecorator.decorate(request, decorator);
}
}
\ No newline at end of file
diff --git a/client/src/test/java/io/split/service/HttpSplitClientTest.java b/client/src/test/java/io/split/service/HttpSplitClientTest.java
index 946775f39..4d18a080d 100644
--- a/client/src/test/java/io/split/service/HttpSplitClientTest.java
+++ b/client/src/test/java/io/split/service/HttpSplitClientTest.java
@@ -16,7 +16,7 @@
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.Header;
+//import org.apache.hc.core5.http.Header;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -57,9 +57,9 @@ public void testGetWithSpecialCharacters() throws URISyntaxException, Invocation
HttpUriRequest request = captor.getValue();
assertThat(request.getFirstHeader("AdditionalHeader").getValue(), is(equalTo("add")));
- Header[] headers = splitHttpResponse.responseHeaders();
+ SplitHttpResponse.Header[] headers = splitHttpResponse.responseHeaders();
assertThat(headers[0].getName(), is(equalTo("Via")));
- assertThat(headers[0].getValue(), is(equalTo("HTTP/1.1 m_proxy_rio1")));
+ assertThat(headers[0].getValues().get(0), is(equalTo("HTTP/1.1 m_proxy_rio1")));
Assert.assertNotNull(change);
Assert.assertEquals(1, change.splits.size());
Assert.assertNotNull(change.splits.get(0));
@@ -122,7 +122,7 @@ public void testPost() throws URISyntaxException, IOException, IllegalAccessExce
Map> additionalHeaders = Collections.singletonMap("SplitSDKImpressionsMode",
Collections.singletonList("OPTIMIZED"));
- SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Utils.toJsonEntity(toSend),
+ SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Json.toJson(toSend),
additionalHeaders);
// Capture outgoing request and validate it
@@ -152,7 +152,7 @@ public void testPosttNoExceptionOnHttpErrorCode() throws URISyntaxException, Inv
SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator, "qwerty", metadata());
SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget,
- Utils.toJsonEntity(Arrays.asList(new String[] { "A", "B", "C", "D" })), null);
+ Json.toJson(Arrays.asList(new String[] { "A", "B", "C", "D" })), null);
Assert.assertEquals(500, (long) splitHttpResponse.statusCode());
}
@@ -165,7 +165,7 @@ public void testPosttException() throws URISyntaxException, InvocationTargetExce
HttpStatus.SC_INTERNAL_SERVER_ERROR);
SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, null, "qwerty", metadata());
- splitHtpClient.post(rootTarget, Utils.toJsonEntity(Arrays.asList(new String[] { "A", "B", "C", "D" })), null);
+ splitHtpClient.post(rootTarget, Json.toJson(Arrays.asList(new String[] { "A", "B", "C", "D" })), null);
}
private SDKMetadata metadata() {
diff --git a/client/src/test/resources/org/powermock/extensions/configuration.properties b/client/src/test/resources/org/powermock/extensions/configuration.properties
new file mode 100644
index 000000000..a8ebaeba3
--- /dev/null
+++ b/client/src/test/resources/org/powermock/extensions/configuration.properties
@@ -0,0 +1 @@
+powermock.global-ignore=jdk.internal.reflect.*,javax.net.ssl.*
\ No newline at end of file
diff --git a/okhttp-modules/pom.xml b/okhttp-modules/pom.xml
new file mode 100644
index 000000000..529869307
--- /dev/null
+++ b/okhttp-modules/pom.xml
@@ -0,0 +1,89 @@
+
+
+
+ java-client-parent
+ io.split.client
+ 4.13.0
+
+ 4.0.0
+ 4.13.0
+ okhttp-modules
+ jar
+ http-modules
+ Alternative Http Modules
+
+
+
+ release
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.3
+ true
+
+ false
+
+
+
+
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+ com.squareup.okhttp3
+ logging-interceptor
+ 4.12.0
+
+
+ io.split.client
+ java-client
+ 4.13.0
+ compile
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.0.3
+
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.8.0
+ test
+
+
+
+
diff --git a/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java
new file mode 100644
index 000000000..26bd23ea5
--- /dev/null
+++ b/okhttp-modules/src/main/java/io/split/httpmodules/okhttp/HTTPKerberosAuthInterceptor.java
@@ -0,0 +1,273 @@
+package io.split.httpmodules.okhttp;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Date;
+import java.util.Set;
+import java.util.Base64;
+
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.Principal;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.Subject;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.kerberos.KerberosTicket;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.Authenticator;
+import okhttp3.Route;
+
+/**
+ *
+ * An HTTP Request interceptor that modifies the request headers to enable
+ * Kerberos authentication. It appends the Kerberos authentication token to the
+ * 'Authorization' request header for Kerberos authentication
+ *
+ * Copyright 2024 MarkLogic Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+public class HTTPKerberosAuthInterceptor implements Authenticator {
+ String host;
+ Map krbOptions;
+ LoginContext loginContext;
+ public HTTPKerberosAuthInterceptor(String host, Map krbOptions) throws IOException {
+ this.host = host;
+ this.krbOptions = krbOptions;
+ try {
+ buildSubjectCredentials();
+ } catch (LoginException e) {
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Class to create Kerberos Configuration object which specifies the Kerberos
+ * Login Module to be used for authentication.
+ *
+ */
+ protected static class KerberosLoginConfiguration extends Configuration {
+ Map krbOptions = null;
+
+ public KerberosLoginConfiguration() {}
+
+ KerberosLoginConfiguration(Map krbOptions) {
+
+ this.krbOptions = krbOptions;
+ }
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ if (krbOptions == null) {
+ throw new IllegalStateException("Cannot create AppConfigurationEntry without Kerberos Options");
+ }
+ return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, krbOptions) };
+ }
+ }
+
+ /**
+ * This method checks the validity of the TGT in the cache and build the
+ * Subject inside the LoginContext using Krb5LoginModule and the TGT cached by
+ * the Kerberos client. It assumes that a valid TGT is already present in the
+ * kerberos client's cache.
+ *
+ * @throws LoginException
+ */
+ protected void buildSubjectCredentials() throws LoginException {
+ Subject subject = new Subject();
+ /**
+ * We are not getting the TGT from KDC here. The actual TGT is got from the
+ * KDC using kinit or equivalent but we use the cached TGT in order to build
+ * the LoginContext and populate the TGT inside the Subject using
+ * Krb5LoginModule
+ */
+
+ LoginContext lc = getLoginContext(subject);
+ lc.login();
+ loginContext = lc;
+ }
+
+ protected LoginContext getLoginContext(Subject subject) throws LoginException {
+ return new LoginContext("Krb5LoginContext", subject, null,
+ (krbOptions != null) ? new KerberosLoginConfiguration(krbOptions) : new KerberosLoginConfiguration());
+ }
+ /**
+ * This method is responsible for getting the client principal name from the
+ * subject's principal set
+ *
+ * @return String the Kerberos principal name populated in the subject
+ * @throws IllegalStateException if there is more than 0 or more than 1
+ * principal is present
+ */
+ protected String getClientPrincipalName() {
+ final Set principalSet = getContextSubject().getPrincipals();
+ if (principalSet.size() != 1)
+ throw new IllegalStateException(
+ "Only one principal is expected. Found 0 or more than one principals :" + principalSet);
+ return principalSet.iterator().next().getName();
+ }
+
+ protected Subject getContextSubject() {
+ Subject subject = loginContext.getSubject();
+ if (subject == null)
+ throw new IllegalStateException("Kerberos login context without subject");
+ return subject;
+ }
+
+ protected CreateAuthorizationHeaderAction getAuthorizationHeaderAction(String clientPrincipal,
+ String serverPrincipalName) {
+ return new CreateAuthorizationHeaderAction(clientPrincipal,
+ serverPrincipalName);
+ }
+
+ /**
+ * This method builds the Authorization header for Kerberos. It
+ * generates a request token based on the service ticket, client principal name and
+ * time-stamp
+ *
+ * @param serverPrincipalName
+ * the name registered with the KDC of the service for which we
+ * need to authenticate
+ * @return the HTTP Authorization header token
+ */
+ protected String buildAuthorizationHeader(String serverPrincipalName) throws LoginException, PrivilegedActionException {
+ /*
+ * Get the principal from the Subject's private credentials and populate the
+ * client and server principal name for the GSS API
+ */
+ final String clientPrincipal = getClientPrincipalName();
+ final CreateAuthorizationHeaderAction action = getAuthorizationHeaderAction(clientPrincipal,
+ serverPrincipalName);
+
+ /*
+ * Check if the TGT in the Subject's private credentials are valid. If
+ * valid, then we use the TGT in the Subject's private credentials. If not,
+ * we build the Subject's private credentials again from valid TGT in the
+ * Kerberos client cache.
+ */
+ Set