diff --git a/client/src/main/java/io/split/client/SplitHttpClient.java b/client/src/main/java/io/split/client/SplitHttpClient.java new file mode 100644 index 000000000..6d54d768f --- /dev/null +++ b/client/src/main/java/io/split/client/SplitHttpClient.java @@ -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 additionalHeaders) throws IOException; +} diff --git a/client/src/main/java/io/split/client/SplitHttpClientImpl.java b/client/src/main/java/io/split/client/SplitHttpClientImpl.java new file mode 100644 index 000000000..a578b1913 --- /dev/null +++ b/client/src/main/java/io/split/client/SplitHttpClientImpl.java @@ -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 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); + } + } +} diff --git a/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java new file mode 100644 index 000000000..a708a4648 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/SplitHttpResponse.java @@ -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; +} diff --git a/client/src/test/java/io/split/client/HttpSplitClientTest.java b/client/src/test/java/io/split/client/HttpSplitClientTest.java new file mode 100644 index 000000000..6a2fd010a --- /dev/null +++ b/client/src/test/java/io/split/client/HttpSplitClientTest.java @@ -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 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 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 additionalHeaders = new HashMap<>(); + additionalHeaders.put("SplitSDKImpressionsMode", "OPTIMIZED"); + SplitHttpResponse splitHttpResponse = splitHtpClient.post(rootTarget, Utils.toJsonEntity(toSend), additionalHeaders); + + // Capture outgoing request and validate it + ArgumentCaptor 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 payload = gson.fromJson(reader, new TypeToken>() { }.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); + } +} \ No newline at end of file