Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions client/src/main/java/io/split/client/SplitHttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.split.client;

import io.split.engine.common.FetchOptions;
import io.split.telemetry.domain.enums.HttpParamsWrapper;
import io.split.client.dtos.SplitHttpResponse;

import org.apache.hc.core5.http.HttpEntity;
import java.io.IOException;
import java.net.URI;
import java.util.Map;

public interface SplitHttpClient {
public SplitHttpResponse get(URI uri, FetchOptions options);
public SplitHttpResponse post
(URI uri,
HttpEntity entity,
Map<String, String> additionalHeaders) throws IOException;
}
119 changes: 119 additions & 0 deletions client/src/main/java/io/split/client/SplitHttpClientImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.split.client;

import io.split.client.exceptions.UriTooLongException;
import io.split.client.utils.Json;
import io.split.client.utils.Utils;
import io.split.engine.common.FetchOptions;
import io.split.client.dtos.SplitHttpResponse;
import org.apache.hc.client5.http.classic.methods.HttpGet;
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.HttpStatus;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public final class SplitHttpClientImpl implements SplitHttpClient {
private static final Logger _log = LoggerFactory.getLogger(SplitHttpClient.class);
private static final String HEADER_CACHE_CONTROL_NAME = "Cache-Control";
private static final String HEADER_CACHE_CONTROL_VALUE = "no-cache";
private final CloseableHttpClient _client;
private final RequestDecorator _requestDecorator;

public static SplitHttpClientImpl create(
CloseableHttpClient client,
RequestDecorator requestDecorator
) throws URISyntaxException {
return new SplitHttpClientImpl(client, requestDecorator);
}

private SplitHttpClientImpl
(CloseableHttpClient client,
RequestDecorator requestDecorator) {
_client = client;
_requestDecorator = requestDecorator;
}

public SplitHttpResponse get(URI uri, FetchOptions options) {
CloseableHttpResponse response = null;

try {
HttpGet request = new HttpGet(uri);
if(options.cacheControlHeadersEnabled()) {
request.setHeader(HEADER_CACHE_CONTROL_NAME, HEADER_CACHE_CONTROL_VALUE);
}
request = (HttpGet) _requestDecorator.decorateHeaders(request);

response = _client.execute(request);

int statusCode = response.getCode();

if (_log.isDebugEnabled()) {
_log.debug(String.format("[%s] %s. Status code: %s", request.getMethod(), uri.toURL(), statusCode));
}

SplitHttpResponse httpResponse = new SplitHttpResponse();
httpResponse.statusMessage = "";
if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) {
if (statusCode == HttpStatus.SC_REQUEST_URI_TOO_LONG) {
_log.error("The amount of flag sets provided are big causing uri length error.");
throw new UriTooLongException(String.format("Status code: %s. Message: %s", statusCode, response.getReasonPhrase()));
}
_log.warn(String.format("Response status was: %s. Reason: %s", statusCode , response.getReasonPhrase()));
httpResponse.statusMessage = response.getReasonPhrase();
}
httpResponse.statusCode = statusCode;
httpResponse.body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
return httpResponse;
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e);
} finally {
Utils.forceClose(response);
}
}

public SplitHttpResponse post
(URI uri,
HttpEntity entity,
Map<String, String> additionalHeaders) throws IOException {
CloseableHttpResponse response = null;
long initTime = System.currentTimeMillis();
try {
HttpPost request = new HttpPost(uri);
if (additionalHeaders != null) {
for (Map.Entry entry : additionalHeaders.entrySet()) {
request.addHeader(entry.getKey().toString(), entry.getValue());
}
}
request.setEntity(entity);
request = (HttpPost) _requestDecorator.decorateHeaders(request);

response = _client.execute(request);

int status = response.getCode();

String statusMessage = new String("");
if (status < HttpStatus.SC_OK || status >= HttpStatus.SC_MULTIPLE_CHOICES) {
statusMessage = response.getReasonPhrase();
_log.warn(String.format("Response status was: %s. Reason: %s", status, response.getReasonPhrase()));
}
SplitHttpResponse httpResponse = new SplitHttpResponse();
httpResponse.statusCode = status;
httpResponse.body = "";
httpResponse.statusMessage = statusMessage;
return httpResponse;
} catch (Exception e) {
throw new IOException(String.format("Problem in http post operation: %s", e), e);
} finally {
Utils.forceClose(response);
}
}
}
10 changes: 10 additions & 0 deletions client/src/main/java/io/split/client/dtos/SplitHttpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.split.client.dtos;

/**
* A structure for returning http call results information
*/
public class SplitHttpResponse {
public Integer statusCode;
public String statusMessage;
public String body;
}
145 changes: 145 additions & 0 deletions client/src/test/java/io/split/client/HttpSplitClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.split.client;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.split.TestHelper;
import io.split.client.dtos.*;
import io.split.client.impressions.Impression;
import io.split.client.utils.Json;
import io.split.client.utils.Utils;
import io.split.engine.common.FetchOptions;
import io.split.telemetry.storage.InMemoryTelemetryStorage;
import io.split.telemetry.storage.TelemetryStorage;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import io.split.telemetry.domain.enums.HttpParamsWrapper;
import org.apache.hc.core5.http.*;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.mockito.Mockito.verify;

public class HttpSplitClientTest {

@Test
public void testGetWithSpecialCharacters() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567");
CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_OK);
RequestDecorator decorator = new RequestDecorator(null);

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator);

SplitHttpResponse splitHttpResponse = splitHtpClient.get(rootTarget,
new FetchOptions.Builder().cacheControlHeaders(true).build());
SplitChange change = Json.fromJson(splitHttpResponse.body, SplitChange.class);

Assert.assertNotNull(change);
Assert.assertEquals(1, change.splits.size());
Assert.assertNotNull(change.splits.get(0));

Split split = change.splits.get(0);
Map<String, String> configs = split.configurations;
Assert.assertEquals(2, configs.size());
Assert.assertEquals("{\"test\": \"blue\",\"grüne Straße\": 13}", configs.get("on"));
Assert.assertEquals("{\"test\": \"blue\",\"size\": 15}", configs.get("off"));
Assert.assertEquals(2, split.sets.size());
}

@Test
public void testGetError() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567");
CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_INTERNAL_SERVER_ERROR);
RequestDecorator decorator = new RequestDecorator(null);

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator);
SplitHttpResponse splitHttpResponse = splitHtpClient.get(rootTarget,
new FetchOptions.Builder().cacheControlHeaders(true).build());
Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, (long) splitHttpResponse.statusCode);
}

@Test(expected = IllegalStateException.class)
public void testException() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567");
CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_INTERNAL_SERVER_ERROR);
RequestDecorator decorator = null;

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator);
splitHtpClient.get(rootTarget,
new FetchOptions.Builder().cacheControlHeaders(true).build());
}

@Test
public void testPost() throws URISyntaxException, IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
URI rootTarget = URI.create("https://kubernetesturl.com/split/api/testImpressions/bulk");

// Setup response mock
CloseableHttpClient httpClient = TestHelper.mockHttpClient("", HttpStatus.SC_OK);
RequestDecorator decorator = new RequestDecorator(null);

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClient, decorator);

// Send impressions
List<TestImpressions> toSend = Arrays.asList(new TestImpressions("t1", Arrays.asList(
KeyImpression.fromImpression(new Impression("k1", null, "t1", "on", 123L, "r1", 456L, null)),
KeyImpression.fromImpression(new Impression("k2", null, "t1", "on", 123L, "r1", 456L, null)),
KeyImpression.fromImpression(new Impression("k3", null, "t1", "on", 123L, "r1", 456L, null))
)), new TestImpressions("t2", Arrays.asList(
KeyImpression.fromImpression(new Impression("k1", null, "t2", "on", 123L, "r1", 456L, null)),
KeyImpression.fromImpression(new Impression("k2", null, "t2", "on", 123L, "r1", 456L, null)),
KeyImpression.fromImpression(new Impression("k3", null, "t2", "on", 123L, "r1", 456L, null))
)));
Map<String, String> additionalHeaders = new HashMap<>();
additionalHeaders.put("SplitSDKImpressionsMode", "OPTIMIZED");
SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Utils.toJsonEntity(toSend), additionalHeaders);

// Capture outgoing request and validate it
ArgumentCaptor<HttpUriRequest> captor = ArgumentCaptor.forClass(HttpUriRequest.class);
verify(httpClient).execute(captor.capture());
HttpUriRequest request = captor.getValue();
assertThat(request.getUri(), is(equalTo(URI.create("https://kubernetesturl.com/split/api/testImpressions/bulk"))));
assertThat(request.getHeaders().length, is(1));
assertThat(request.getFirstHeader("SplitSDKImpressionsMode").getValue(), is(equalTo("OPTIMIZED")));
assertThat(request, instanceOf(HttpPost.class));
HttpPost asPostRequest = (HttpPost) request;
InputStreamReader reader = new InputStreamReader(asPostRequest.getEntity().getContent());
Gson gson = new Gson();
List<TestImpressions> payload = gson.fromJson(reader, new TypeToken<List<TestImpressions>>() { }.getType());
assertThat(payload.size(), is(equalTo(2)));
Assert.assertEquals(200,(long) splitHttpResponse.statusCode);
}

@Test
public void testPosttNoExceptionOnHttpErrorCode() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567");
CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_INTERNAL_SERVER_ERROR);
RequestDecorator decorator = new RequestDecorator(null);

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, decorator);
SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Utils.toJsonEntity(Arrays.asList( new String[] { "A", "B", "C", "D" })), null);
Assert.assertEquals(500, (long) splitHttpResponse.statusCode);

}

@Test(expected = IOException.class)
public void testPosttException() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException {
URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567");
CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_INTERNAL_SERVER_ERROR);

SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, null);
splitHtpClient.post(rootTarget, Utils.toJsonEntity(Arrays.asList( new String[] { "A", "B", "C", "D" })), null);
}
}