diff --git a/README.md b/README.md index ec41672..9ddd4af 100644 --- a/README.md +++ b/README.md @@ -95,16 +95,19 @@ Take in account that you need to use **Java 11 or greater**. We have the following attributes to create a CleverClient object: -| Attribute | Description | Required | -| -------------------|----------------------------------------------|-----------| -| baseUrl | Api's url | mandatory | -| headers | Map of headers (name/value) | optional | -| header | Single header (alternative to headers) | optional | -| httpClient | Java HttpClient object | optional | -| requestInterceptor | Function to modify the request once is built | optional | -| endOfStream | Text used to mark the final of streams | optional | - -The attribute ```endOfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE). +| Attribute | Description | Required | +| -------------------|--------------------------------------------------|-----------| +| baseUrl | Api's url | mandatory | +| headers | Map of headers (name/value) | optional | +| header | Single header as a name and a value | optional | +| httpClient | Java HttpClient object | optional | +| requestInterceptor | Function to modify the request once is built | optional | +| eventsToRead | List of events's name we want to read in streams | optional | +| eventToRead | An event's name we want to read in streams | optional | +| endsOfStream | List of texts used to mark the end of streams | optional | +| endOfStream | Text used to mark the end of streams | optional | + +The attributes ```event(s)ToRead``` and ```end(s)OfStream``` are required when you have endpoints sending back streams of data (Server Sent Events - SSE). Example: @@ -112,6 +115,7 @@ Example: final var BASE_URL = "https://api.example.com"; final var HEADER_NAME = "Authorization"; final var HEADER_VALUE = "Bearer qwertyasdfghzxcvb"; +final var EVENT_TO_READ = "inventory"; final var END_OF_STREAM = "[DONE]"; var httpClient = HttpClient.newBuilder() @@ -132,6 +136,7 @@ var cleverClient = CleverClient.builder() request.setUrl(url); return request; }) + .eventToRead(EVENT_TO_READ) .endOfStream(END_OF_STREAM) .build(); ``` diff --git a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java index a727950..e31d7e5 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java +++ b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java @@ -1,6 +1,8 @@ package io.github.sashirestela.cleverclient; import java.net.http.HttpClient; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.UnaryOperator; @@ -10,7 +12,7 @@ import io.github.sashirestela.cleverclient.http.HttpProcessor; import io.github.sashirestela.cleverclient.http.HttpRequestData; -import io.github.sashirestela.cleverclient.support.CleverClientSSE; +import io.github.sashirestela.cleverclient.support.Configurator; import io.github.sashirestela.cleverclient.util.CommonUtil; import lombok.Builder; import lombok.Getter; @@ -26,7 +28,6 @@ public class CleverClient { private static final Logger logger = LoggerFactory.getLogger(CleverClient.class); - @NonNull private final String baseUrl; private final Map headers; private final HttpClient httpClient; @@ -48,18 +49,22 @@ public class CleverClient { */ @Builder public CleverClient(@NonNull String baseUrl, @Singular Map headers, HttpClient httpClient, - UnaryOperator requestInterceptor, String endOfStream) { + UnaryOperator requestInterceptor, @Singular("eventToRead") List eventsToRead, + @Singular("endOfStream") List endsOfStream) { this.baseUrl = baseUrl; this.headers = Optional.ofNullable(headers).orElse(Map.of()); this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient()); this.requestInterceptor = requestInterceptor; - CleverClientSSE.setEndOfStream(endOfStream); this.httpProcessor = HttpProcessor.builder() .baseUrl(this.baseUrl) .headers(CommonUtil.mapToListOfString(this.headers)) .httpClient(this.httpClient) .requestInterceptor(this.requestInterceptor) .build(); + Configurator.builder() + .eventsToRead(Optional.ofNullable(eventsToRead).orElse(Arrays.asList())) + .endsOfStream(Optional.ofNullable(endsOfStream).orElse(Arrays.asList())) + .build(); logger.debug("CleverClient has been created."); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java index ce61a04..e363cf2 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java @@ -20,11 +20,14 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, throwExceptionIfErrorIsPresent(response, Stream.class); + final var record = new CleverClientSSE.LineRecord(); + return response.body() - .peek(rawData -> logger.debug("Response : {}", rawData)) - .map(CleverClientSSE::new) + .peek(line -> logger.debug("Response : {}", line)) + .peek(line -> record.updateWith(line)) + .map(line -> new CleverClientSSE(record)) .filter(CleverClientSSE::isActualData) - .map(event -> JsonUtil.jsonToObject(event.getActualData(), responseClass)); + .map(item -> JsonUtil.jsonToObject(item.getActualData(), responseClass)); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java index 181f40d..5c30f24 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java @@ -21,11 +21,14 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, throwExceptionIfErrorIsPresent(httpResponse, Stream.class); + final var record = new CleverClientSSE.LineRecord(); + return httpResponse.body() - .peek(rawData -> logger.debug("Response : {}", rawData)) - .map(CleverClientSSE::new) + .peek(line -> logger.debug("Response : {}", line)) + .peek(line -> record.updateWith(line)) + .map(line -> new CleverClientSSE(record)) .filter(CleverClientSSE::isActualData) - .map(event -> JsonUtil.jsonToObject(event.getActualData(), responseClass)); + .map(item -> JsonUtil.jsonToObject(item.getActualData(), responseClass)); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java index 77fa179..ca08458 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java @@ -5,6 +5,10 @@ public class CleverClientException extends RuntimeException { + public CleverClientException(String message) { + super(message); + } + public CleverClientException(String message, Object... parameters) { super(MessageFormat.format(message, Arrays.copyOfRange(parameters, 0, parameters.length - 1)), (Throwable) parameters[parameters.length - 1]); diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java index 1c8294a..7b44c11 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java @@ -1,31 +1,72 @@ package io.github.sashirestela.cleverclient.support; +import java.util.List; +import java.util.stream.Collectors; + public class CleverClientSSE { + private static final String EVENT_HEADER = "event: "; private static final String DATA_HEADER = "data: "; + private static final String SEPARATOR = ""; + + private static List linesToCheck = null; - private static String endOfStream = null; + private LineRecord record; + private List eventsToRead; + private List endsOfStream; - private String rawData; + public CleverClientSSE(LineRecord record) { + this.record = record; + this.eventsToRead = Configurator.one().getEventsToRead(); + this.endsOfStream = Configurator.one().getEndsOfStream(); - public CleverClientSSE(String rawData) { - this.rawData = rawData; + if (linesToCheck == null) { + linesToCheck = this.eventsToRead.stream().filter(etr -> !etr.isEmpty()).map(etr -> (EVENT_HEADER + etr)) + .collect(Collectors.toList()); + linesToCheck.add(SEPARATOR); + } } - public String getRawData() { - return rawData; + public LineRecord getRecord() { + return record; } public boolean isActualData() { - return rawData.startsWith(DATA_HEADER) && (endOfStream == null || !rawData.contains(endOfStream)); + return linesToCheck.contains(record.previous()) && record.current().startsWith(DATA_HEADER) + && endsOfStream.stream().anyMatch(eos -> !record.current().contains(eos)); } public String getActualData() { - return rawData.replace(DATA_HEADER, "").strip(); + return record.current().replace(DATA_HEADER, "").strip(); } - public static void setEndOfStream(String endOfStream) { - CleverClientSSE.endOfStream = endOfStream; + public static class LineRecord { + + private String currentLine; + private String previousLine; + + public LineRecord(String previousLine, String currentLine) { + this.previousLine = previousLine; + this.currentLine = currentLine; + } + + public LineRecord() { + this("", ""); + } + + public void updateWith(String line) { + this.previousLine = this.currentLine; + this.currentLine = line; + } + + public String current() { + return this.currentLine; + } + + public String previous() { + return this.previousLine; + } + } -} \ No newline at end of file +} diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java b/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java new file mode 100644 index 0000000..f5266d4 --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java @@ -0,0 +1,37 @@ +package io.github.sashirestela.cleverclient.support; + +import java.util.List; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +@Getter +public class Configurator { + + private static Configurator configurator; + + private List eventsToRead; + private List endsOfStream; + + private Configurator() { + } + + @Builder + public Configurator(@Singular("eventToRead") List eventsToRead, + @Singular("endOfStream") List endsOfStream) { + if (configurator != null) { + return; + } + configurator = new Configurator(); + configurator.eventsToRead = eventsToRead; + configurator.endsOfStream = endsOfStream; + } + + public static Configurator one() { + if (configurator == null) { + throw new CleverClientException("You have to call Configurator.builder() first."); + } + return configurator; + } +} diff --git a/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java b/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java index a99e350..3b027f4 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java @@ -2,37 +2,38 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import io.github.sashirestela.cleverclient.support.CleverClientSSE.LineRecord; + class CleverClientSSETest { - @Test - void shouldReturnExpectedValueWhenRawDataHasDifferentValues() { - Object[][] testData = { - { new CleverClientSSE("data: This is the actual data."), true }, - { new CleverClientSSE("data : This is the actual data."), false }, - { new CleverClientSSE("\n"), false }, - { new CleverClientSSE(""), false } - }; - for (Object[] data : testData) { - CleverClientSSE event = (CleverClientSSE) data[0]; - boolean actualCondition = event.isActualData(); - boolean expectedCondition = (boolean) data[1]; - assertEquals(expectedCondition, actualCondition); - } + @BeforeAll + static void setup() { + Configurator.builder() + .eventToRead("process") + .endOfStream("END") + .build(); } @Test - void shouldReturnExpectedValueWhenRawDataHasDifferentValuesAndAEndOfStreamIsSetted() { - CleverClientSSE.setEndOfStream("END"); + void shouldReturnExpectedValueWhenRawDataHasDifferentValues() { Object[][] testData = { - { new CleverClientSSE("data: This is the actual data."), true }, - { new CleverClientSSE("data: END"), false } + { new CleverClientSSE(new LineRecord("event: process", "data: This is the actual data.")), true }, + { new CleverClientSSE(new LineRecord("", "data: This is the actual data.")), true }, + { new CleverClientSSE(new LineRecord("event: other", "data: This is the actual data.")), false }, + { new CleverClientSSE(new LineRecord("event: process", "data : This is the actual data.")), false }, + { new CleverClientSSE(new LineRecord("", "data : This is the actual data.")), false }, + { new CleverClientSSE(new LineRecord("", "\n")), false }, + { new CleverClientSSE(new LineRecord("", "")), false }, + { new CleverClientSSE(new LineRecord("event: process", "data: END")), false }, + { new CleverClientSSE(new LineRecord("", "data: END")), false } }; for (Object[] data : testData) { - CleverClientSSE event = (CleverClientSSE) data[0]; - boolean actualCondition = event.isActualData(); - boolean expectedCondition = (boolean) data[1]; + var event = (CleverClientSSE) data[0]; + var actualCondition = event.isActualData(); + var expectedCondition = (boolean) data[1]; assertEquals(expectedCondition, actualCondition); } } @@ -40,10 +41,10 @@ void shouldReturnExpectedValueWhenRawDataHasDifferentValuesAndAEndOfStreamIsSett @Test @SuppressWarnings("unused") void shouldReturnTheActualDataWhenRawDataMeetsConditions() { - CleverClientSSE event = new CleverClientSSE("data: This is the actual data. "); - String rawData = event.getRawData(); - String actualData = event.getActualData(); - String expectedData = "This is the actual data."; + CleverClientSSE event = new CleverClientSSE(new LineRecord("event: process", "data: This is the actual data. ")); + var rawData = event.getRecord(); + var actualData = event.getActualData(); + var expectedData = "This is the actual data."; assertEquals(expectedData, actualData); } } \ No newline at end of file