From 7e344d361f416657a0c8468afccb688ae9d9d758 Mon Sep 17 00:00:00 2001 From: Sashir Estela Date: Fri, 22 Mar 2024 02:48:46 +0000 Subject: [PATCH] Handling SSE with multiple events --- README.md | 96 ++++++++++--------- .../cleverclient/example/StreamExample.java | 2 +- .../cleverclient/CleverClient.java | 6 +- .../cleverclient/annotation/StreamType.java | 31 ++++++ .../cleverclient/http/HttpConnector.java | 4 +- .../metadata/InterfaceMetadataStore.java | 2 +- .../sender/HttpAsyncBinarySender.java | 8 +- ...Sender.java => HttpAsyncCustomSender.java} | 8 +- .../sender/HttpAsyncGenericSender.java | 7 +- .../sender/HttpAsyncListSender.java | 6 +- .../sender/HttpAsyncPlainTextSender.java | 8 +- .../sender/HttpAsyncStreamObjectSender.java | 39 ++++++++ .../sender/HttpAsyncStreamSender.java | 9 +- .../cleverclient/sender/HttpSender.java | 13 +-- .../sender/HttpSenderFactory.java | 6 +- .../sender/HttpSyncBinarySender.java | 4 +- ...tSender.java => HttpSyncCustomSender.java} | 8 +- .../sender/HttpSyncGenericSender.java | 7 +- .../sender/HttpSyncListSender.java | 6 +- .../sender/HttpSyncPlainTextSender.java | 4 +- .../sender/HttpSyncStreamObjectSender.java | 44 +++++++++ .../sender/HttpSyncStreamSender.java | 9 +- .../cleverclient/support/CleverClientSSE.java | 19 +++- .../cleverclient/support/Configurator.java | 22 +---- .../cleverclient/support/ReturnType.java | 73 ++++++++++---- .../cleverclient/http/HttpProcessorTest.java | 53 ++++++++++ .../sashirestela/cleverclient/http/ITest.java | 10 ++ .../support/CleverClientSSETest.java | 40 +++++--- .../cleverclient/support/ReturnTypeTest.java | 60 +++++++++--- 29 files changed, 431 insertions(+), 173 deletions(-) create mode 100644 src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java rename src/main/java/io/github/sashirestela/cleverclient/sender/{HttpAsyncObjectSender.java => HttpAsyncCustomSender.java} (63%) create mode 100644 src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamObjectSender.java rename src/main/java/io/github/sashirestela/cleverclient/sender/{HttpSyncObjectSender.java => HttpSyncCustomSender.java} (72%) create mode 100644 src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamObjectSender.java diff --git a/README.md b/README.md index 8832860..94bd9db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 💎 CleverClient -Java library that makes it easier to use the Java's HttpClient to perform http operations, using interfaces. +Library that makes it easy to use the Java HttpClient to perform http operations through interfaces. [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=sashirestela_cleverclient&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=sashirestela_cleverclient) [![codecov](https://codecov.io/gh/sashirestela/cleverclient/graph/badge.svg?token=PEYAFW3EWD)](https://codecov.io/gh/sashirestela/cleverclient) @@ -23,7 +23,7 @@ Java library that makes it easier to use the Java's HttpClient to perform http o CleverClient is a Java 11+ library that makes it easy to use the standard [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html) component to call http services by using annotated interfaces. -For example, if we want to use the API [JsonPlaceHolder](https://jsonplaceholder.typicode.com/) and call the endpoint ```/posts```, we just have to create an entity ```Post```, an interface ```PostService``` with special annotatons, and call the API through ```CleverClient```: +For example, if we want to use the public API [JsonPlaceHolder](https://jsonplaceholder.typicode.com/) and call the endpoint ```/posts```, we just have to create an entity ```Post```, an interface ```PostService``` with special annotatons, and call the API through ```CleverClient```: ```java // Entity @@ -115,12 +115,10 @@ We have the following attributes to create a CleverClient object: | 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). +The attribute ```end(s)OfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE). Example: @@ -128,7 +126,6 @@ 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() @@ -149,32 +146,33 @@ var cleverClient = CleverClient.builder() request.setUrl(url); return request; }) - .eventToRead(EVENT_TO_READ) .endOfStream(END_OF_STREAM) .build(); ``` ### Interface Annotations -| Annotation | Target | Value | Required Value | -|------------|-----------|-----------------------------|----------------| -| Resource | Interface | Resource's url | optional | -| Header | Interface | Header's name and value | mandatory | -| Header | Method | Header's name and value | mandatory | -| GET | Method | GET endpoint's url | optional | -| POST | Method | POST endpoint's url | optional | -| PUT | Method | PUT endpoint's url | optional | -| DELETE | Method | DELETE endpoint's url | optional | -| Multipart | Method | (None) | none | -| Path | Parameter | Path parameter name in url | mandatory | -| Query | Parameter | Query parameter name in url | mandatory | -| Query | Parameter | (None for Pojos) | none | -| Body | Parameter | (None) | none | +| Annotation | Target | Attributes | Required Attrs | Mult | +|------------|-----------|-----------------------------|----------------|------| +| Resource | Interface | Resource's url | optional | One | +| Header | Interface | Header's name and value | mandatory both | Many | +| Header | Method | Header's name and value | mandatory both | Many | +| GET | Method | GET endpoint's url | optional | One | +| POST | Method | POST endpoint's url | optional | One | +| PUT | Method | PUT endpoint's url | optional | One | +| DELETE | Method | DELETE endpoint's url | optional | One | +| Multipart | Method | (None) | none | One | +| StreamType | Method | type class and events array | mandatory both | Many | +| Path | Parameter | Path parameter name in url | mandatory | One | +| Query | Parameter | Query parameter name in url | mandatory | One | +| Query | Parameter | (None for Pojos) | none | One | +| Body | Parameter | (None) | none | One | * ```Resource``` could be used to separate the repeated part of the endpoints' url in an interface. * ```Header``` Used to include more headers (pairs of name and value) at interface or method level. It is possible to have multiple Header annotations for the same target. * ```GET, POST, PUT, DELETE``` are used to mark the typical http methods (endpoints). * ```Multipart``` is used to mark an endpoint with a multipart/form-data request. This is required when you need to upload files. +* ```StreamType``` is used with methods whose return type is Stream of Objects. Commonly you will use more than one to indicate what class (type) is related to what events (array of Strings). * ```Path``` is used to replace the path parameter name in url with the matched method parameter's value. * ```Query``` is used to add a query parameter to the url in the way: [?]queryValue=parameterValue[&...] for scalar parameters. Also it can be used for POJOs using its properties and values. * ```Body``` is used to mark a method parameter as the endpoint's payload request, so the request will be application/json at least the endpoint is annotated with Multipart. @@ -182,22 +180,28 @@ var cleverClient = CleverClient.builder() ### Supported Response Types -The reponse types are determined from the method responses. We don't need any annotation for that. We have six response types: [Stream](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html) of elements, [List](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html) of elements, [Generic](https://docs.oracle.com/javase/tutorial/java/generics/types.html) type, Single Class, [Binary](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/InputStream.html) object, and [String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) object, and all of them can be asynchronous or synchronous. For async responses you have to use the Java class [CompletableFuture](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html). - -| Method Response | Sync/Async | Response Type | -|---------------------------------|------------|----------------------------------------| -| CompletableFuture> | Async | Server sent events as Stream of type T | -| Stream\ | Sync | Server sent events as Stream of type T | -| CompletableFuture> | Async | List of type T | -| List\ | Sync | List of type T | -| CompletableFuture> | Async | Generic class of type T | -| Generic\ | Sync | Generic class of type T | -| CompletableFuture\ | Async | Single class T | -| T | Sync | Single class T | -| CompletableFuture\ | Async | Binary Object | -| InputStream | Sync | Binary Object | -| CompletableFuture\ | Async | String Object | -| String | Sync | String Object | +The reponse types are determined from the method responses. We don't need any annotation for that. We have six response types: [Stream](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html) of elements, [List](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html) of elements, [Generic](https://docs.oracle.com/javase/tutorial/java/generics/types.html) type, Custom type, [Binary](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/InputStream.html) type, [String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html) type and Stream of [Object](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html), and all of them can be asynchronous or synchronous. For async responses you have to use the Java class [CompletableFuture](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html). + +| Method's Response Type | Sync/Async | Description | +|------------------------------------|------------|-----------------------------| +| CompletableFuture> | Async | SSE (*) as Stream of type T | +| Stream\ | Sync | SSE (*) as Stream of type T | +| CompletableFuture> | Async | List of type T | +| List\ | Sync | List of type T | +| CompletableFuture> | Async | Generic class of type T | +| Generic\ | Sync | Generic class of type T | +| CompletableFuture\ | Async | Custom class T | +| T | Sync | Custom class T | +| CompletableFuture\ | Async | Binary type | +| InputStream | Sync | Binary type | +| CompletableFuture\ | Async | String type | +| String | Sync | String type | +| CompletableFuture> | Async | SSE (*) as Stream of Object | +| Stream\ | Sync | SSE (*) as Stream of Object | + +(*) SSE: Server Sent Events + +```CompletableFuture>``` and ```Stream``` are used for handling SSE with multiple events and classes. ### Interface Default Methods @@ -207,26 +211,26 @@ You can create interface default methods to execute special requirements such as @Resource("/v1/chat/completions") interface Completions { - @POST - Stream __createSyncStream(@Body ChatRequest chatRequest); - default Stream createSyncStream(ChatRequest chatRequest) { var request = chatRequest.withStream(true); - return __createSyncStream(request); + return createSyncStreamBasic(request); } - @POST - CompletableFuture> __createAsyncStream(@Body ChatRequest chatRequest); - default CompletableFuture> createAsyncStream(ChatRequest chatRequest) { var request = chatRequest.withStream(true); - return __createAsyncStream(request); + return createAsyncStreamBasic(request); } + @POST + Stream createSyncStreamBasic(@Body ChatRequest chatRequest); + + @POST + CompletableFuture> createAsyncStreamBasic(@Body ChatRequest chatRequest); + } ``` -Note that we have named the annotated methods with the suffix "__" just to indicate that we should not call them directly but should call the default ones (those without the suffix). +Note that we have named the annotated methods with the suffix "Basic" just to indicate that we should not call them directly but should call the default ones (those without the suffix). ## ✳ Examples diff --git a/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java b/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java index f421f4f..3e24aba 100644 --- a/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java +++ b/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java @@ -12,7 +12,7 @@ * Before running this example you must have an OpenAI account and keep your Api Key in an * environment variable called OPENAI_API_KEY. * - * @see https://platform.openai.com/docs/api-reference/authentication + * @see OpenAI Authentication */ public class StreamExample { diff --git a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java index c1dbb38..2acf2c1 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java +++ b/src/main/java/io/github/sashirestela/cleverclient/CleverClient.java @@ -41,15 +41,12 @@ public class CleverClient { * @param httpClient Custom Java's HttpClient component. One is created by default if none * is passed. Optional. * @param requestInterceptor Function to modify the request once it has been built. - * @param eventsToRead List of events's name that we want to read from server sent events - * (SSE). Optional. * @param endsOfStream Texts used to mark the final of streams when handling server sent * events (SSE). Optional. */ @Builder public CleverClient(@NonNull String baseUrl, @Singular Map headers, HttpClient httpClient, - UnaryOperator requestInterceptor, @Singular("eventToRead") List eventsToRead, - @Singular("endOfStream") List endsOfStream) { + UnaryOperator requestInterceptor, @Singular("endOfStream") List endsOfStream) { this.baseUrl = baseUrl; this.headers = Optional.ofNullable(headers).orElse(Map.of()); this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient()); @@ -61,7 +58,6 @@ public CleverClient(@NonNull String baseUrl, @Singular Map heade .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/annotation/StreamType.java b/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java new file mode 100644 index 0000000..c78e0a9 --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java @@ -0,0 +1,31 @@ +package io.github.sashirestela.cleverclient.annotation; + +import io.github.sashirestela.cleverclient.annotation.StreamType.List; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(List.class) +public @interface StreamType { + + Class type(); + + String[] events(); + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface List { + + StreamType[] value(); + + } + +} diff --git a/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java b/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java index 3586cd9..fb7f5b8 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java +++ b/src/main/java/io/github/sashirestela/cleverclient/http/HttpConnector.java @@ -53,8 +53,6 @@ public Object sendRequest() { logger.debug("Request Headers : {}", formattedHeaders); var bodyPublisher = createBodyPublisher(bodyObject, contentType); - var responseClass = returnType.getBaseClass(); - var genericClass = returnType.getGenericClassIfExists(); var headersArray = headers.toArray(new String[0]); HttpRequest httpRequest = null; if (headersArray.length > 0) { @@ -70,7 +68,7 @@ public Object sendRequest() { .build(); } var httpSender = HttpSenderFactory.get().createSender(returnType); - return httpSender.sendRequest(httpClient, httpRequest, responseClass, genericClass); + return httpSender.sendRequest(httpClient, httpRequest, returnType); } private void interceptRequest() { diff --git a/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadataStore.java b/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadataStore.java index 3175775..61e8eaa 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadataStore.java +++ b/src/main/java/io/github/sashirestela/cleverclient/metadata/InterfaceMetadataStore.java @@ -46,7 +46,7 @@ public void save(Class interfaceClass) { for (var javaMethod : interfaceClass.getMethods()) { var methodMetadata = MethodMetadata.builder() .name(javaMethod.getName()) - .returnType(new ReturnType(javaMethod.getGenericReturnType().getTypeName())) + .returnType(new ReturnType(javaMethod)) .isDefault(javaMethod.isDefault()) .annotations(getAnnotations(javaMethod.getDeclaredAnnotations())) .parameters(getParameters(javaMethod.getParameters())) diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncBinarySender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncBinarySender.java index 8046a4e..1e199c1 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncBinarySender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncBinarySender.java @@ -1,5 +1,7 @@ package io.github.sashirestela.cleverclient.sender; +import io.github.sashirestela.cleverclient.support.ReturnType; + import java.io.InputStream; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -8,9 +10,7 @@ public class HttpAsyncBinarySender extends HttpSender { @Override - @SuppressWarnings("unchecked") - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofInputStream()); @@ -20,7 +20,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", response.body()); - return (T) response.body(); + return response.body(); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncObjectSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncCustomSender.java similarity index 63% rename from src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncObjectSender.java rename to src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncCustomSender.java index d80e3cb..7e46415 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncObjectSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncCustomSender.java @@ -1,16 +1,16 @@ package io.github.sashirestela.cleverclient.sender; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; -public class HttpAsyncObjectSender extends HttpSender { +public class HttpAsyncCustomSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofString()); @@ -20,7 +20,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", response.body()); - return JsonUtil.jsonToObject(response.body(), responseClass); + return JsonUtil.jsonToObject(response.body(), returnType.getBaseClass()); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncGenericSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncGenericSender.java index ffc7b84..c8eb13a 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncGenericSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncGenericSender.java @@ -1,5 +1,6 @@ package io.github.sashirestela.cleverclient.sender; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.net.http.HttpClient; @@ -9,8 +10,7 @@ public class HttpAsyncGenericSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofString()); @@ -20,7 +20,8 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", response.body()); - return JsonUtil.jsonToParametricObject(response.body(), genericClass, responseClass); + return JsonUtil.jsonToParametricObject(response.body(), returnType.getGenericClassIfExists(), + returnType.getBaseClass()); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncListSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncListSender.java index 9bef7ee..f3ddccd 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncListSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncListSender.java @@ -1,5 +1,6 @@ package io.github.sashirestela.cleverclient.sender; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.net.http.HttpClient; @@ -9,8 +10,7 @@ public class HttpAsyncListSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofString()); @@ -20,7 +20,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", response.body()); - return JsonUtil.jsonToList(response.body(), responseClass); + return JsonUtil.jsonToList(response.body(), returnType.getBaseClass()); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncPlainTextSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncPlainTextSender.java index 44bd44f..7d68c4d 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncPlainTextSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncPlainTextSender.java @@ -1,5 +1,7 @@ package io.github.sashirestela.cleverclient.sender; +import io.github.sashirestela.cleverclient.support.ReturnType; + import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; @@ -7,9 +9,7 @@ public class HttpAsyncPlainTextSender extends HttpSender { @Override - @SuppressWarnings("unchecked") - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofString()); @@ -19,7 +19,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", response.body()); - return (T) response.body(); + return response.body(); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamObjectSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamObjectSender.java new file mode 100644 index 0000000..c97eb3f --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamObjectSender.java @@ -0,0 +1,39 @@ +package io.github.sashirestela.cleverclient.sender; + +import io.github.sashirestela.cleverclient.support.CleverClientSSE; +import io.github.sashirestela.cleverclient.support.CleverClientSSE.LineRecord; +import io.github.sashirestela.cleverclient.support.ReturnType; +import io.github.sashirestela.cleverclient.util.JsonUtil; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.stream.Stream; + +public class HttpAsyncStreamObjectSender extends HttpSender { + + @Override + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { + + var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofLines()); + + return httpResponseFuture.thenApply(response -> { + + throwExceptionIfErrorIsPresent(response, Stream.class); + + final var lineRecord = new LineRecord(); + final var eventsWithHeader = returnType.getClassByEvent().keySet(); + + return response.body() + .map(line -> { + logger.debug("Response : {}", line); + lineRecord.updateWith(line); + return new CleverClientSSE(lineRecord, eventsWithHeader); + }) + .filter(CleverClientSSE::isActualData) + .map(item -> JsonUtil.jsonToObject(item.getActualData(), + returnType.getClassByEvent().get(item.getMatchedEvent()))); + }); + } + +} 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 2634c7c..f565ca7 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpAsyncStreamSender.java @@ -1,6 +1,8 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientSSE; +import io.github.sashirestela.cleverclient.support.CleverClientSSE.LineRecord; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.net.http.HttpClient; @@ -11,8 +13,7 @@ public class HttpAsyncStreamSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { var httpResponseFuture = httpClient.sendAsync(httpRequest, BodyHandlers.ofLines()); @@ -20,7 +21,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, throwExceptionIfErrorIsPresent(response, Stream.class); - final var lineRecord = new CleverClientSSE.LineRecord(); + final var lineRecord = new LineRecord(); return response.body() .map(line -> { @@ -29,7 +30,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, return new CleverClientSSE(lineRecord); }) .filter(CleverClientSSE::isActualData) - .map(item -> JsonUtil.jsonToObject(item.getActualData(), responseClass)); + .map(item -> JsonUtil.jsonToObject(item.getActualData(), returnType.getBaseClass())); }); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java index 3c87e57..4758954 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.CommonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,16 +28,12 @@ public abstract class HttpSender { * Method to be implementd for concrete classes to send request to the Java's HttpClient and receive * response. * - * @param Type of a generic class if exists. - * @param Type of the response. - * @param httpClient Java's HttpClient component. - * @param httpRequest Java's HttpRequest component. - * @param responseClass Response class. - * @param genericClass Generic class if exists. + * @param httpClient Java's HttpClient component. + * @param httpRequest Java's HttpRequest component. + * @param returnType Response class and generic class if exists. * @return Response coming from Java's HttpClient. */ - public abstract Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass); + public abstract Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType); /** * Exception handling that will be called by any concrete class. diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSenderFactory.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSenderFactory.java index ab966d9..b767855 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSenderFactory.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSenderFactory.java @@ -22,16 +22,18 @@ public class HttpSenderFactory { private HttpSenderFactory() { sendersMap = new EnumMap<>(Category.class); + sendersMap.put(Category.ASYNC_STREAM_OBJECT, HttpAsyncStreamObjectSender::new); sendersMap.put(Category.ASYNC_STREAM, HttpAsyncStreamSender::new); sendersMap.put(Category.ASYNC_LIST, HttpAsyncListSender::new); sendersMap.put(Category.ASYNC_GENERIC, HttpAsyncGenericSender::new); - sendersMap.put(Category.ASYNC_OBJECT, HttpAsyncObjectSender::new); + sendersMap.put(Category.ASYNC_CUSTOM, HttpAsyncCustomSender::new); sendersMap.put(Category.ASYNC_BINARY, HttpAsyncBinarySender::new); sendersMap.put(Category.ASYNC_PLAIN_TEXT, HttpAsyncPlainTextSender::new); + sendersMap.put(Category.SYNC_STREAM_OBJECT, HttpSyncStreamObjectSender::new); sendersMap.put(Category.SYNC_STREAM, HttpSyncStreamSender::new); sendersMap.put(Category.SYNC_LIST, HttpSyncListSender::new); sendersMap.put(Category.SYNC_GENERIC, HttpSyncGenericSender::new); - sendersMap.put(Category.SYNC_OBJECT, HttpSyncObjectSender::new); + sendersMap.put(Category.SYNC_CUSTOM, HttpSyncCustomSender::new); sendersMap.put(Category.SYNC_BINARY, HttpSyncBinarySender::new); sendersMap.put(Category.SYNC_PLAIN_TEXT, HttpSyncPlainTextSender::new); } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java index 71600e8..c10e9db 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import java.io.IOException; import java.io.InputStream; @@ -11,8 +12,7 @@ public class HttpSyncBinarySender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofInputStream()); diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncObjectSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java similarity index 72% rename from src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncObjectSender.java rename to src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java index 9e9e934..2f125cb 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncObjectSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.io.IOException; @@ -8,11 +9,10 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; -public class HttpSyncObjectSender extends HttpSender { +public class HttpSyncCustomSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); @@ -23,7 +23,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", rawData); - return JsonUtil.jsonToObject(rawData, responseClass); + return JsonUtil.jsonToObject(rawData, returnType.getBaseClass()); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java index b733031..e5c9a72 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.io.IOException; @@ -11,8 +12,7 @@ public class HttpSyncGenericSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); @@ -23,7 +23,8 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", rawData); - return JsonUtil.jsonToParametricObject(rawData, genericClass, responseClass); + return JsonUtil.jsonToParametricObject(rawData, returnType.getGenericClassIfExists(), + returnType.getBaseClass()); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java index b1bee1f..420928c 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.io.IOException; @@ -11,8 +12,7 @@ public class HttpSyncListSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); @@ -23,7 +23,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, logger.debug("Response : {}", rawData); - return JsonUtil.jsonToList(rawData, responseClass); + return JsonUtil.jsonToList(rawData, returnType.getBaseClass()); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java index d87cdf8..6c24481 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java @@ -1,6 +1,7 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.ReturnType; import java.io.IOException; import java.net.http.HttpClient; @@ -10,8 +11,7 @@ public class HttpSyncPlainTextSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamObjectSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamObjectSender.java new file mode 100644 index 0000000..c031405 --- /dev/null +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamObjectSender.java @@ -0,0 +1,44 @@ +package io.github.sashirestela.cleverclient.sender; + +import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.CleverClientSSE; +import io.github.sashirestela.cleverclient.support.CleverClientSSE.LineRecord; +import io.github.sashirestela.cleverclient.support.ReturnType; +import io.github.sashirestela.cleverclient.util.JsonUtil; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.stream.Stream; + +public class HttpSyncStreamObjectSender extends HttpSender { + + @Override + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { + try { + + var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofLines()); + + throwExceptionIfErrorIsPresent(httpResponse, Stream.class); + + final var lineRecord = new LineRecord(); + final var eventsWithHeader = returnType.getClassByEvent().keySet(); + + return httpResponse.body() + .map(line -> { + logger.debug("Response : {}", line); + lineRecord.updateWith(line); + return new CleverClientSSE(lineRecord, eventsWithHeader); + }) + .filter(CleverClientSSE::isActualData) + .map(item -> JsonUtil.jsonToObject(item.getActualData(), + returnType.getClassByEvent().get(item.getMatchedEvent()))); + + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CleverClientException(e.getMessage(), null, e); + } + } + +} 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 8133268..f8bd923 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java @@ -2,6 +2,8 @@ import io.github.sashirestela.cleverclient.support.CleverClientException; import io.github.sashirestela.cleverclient.support.CleverClientSSE; +import io.github.sashirestela.cleverclient.support.CleverClientSSE.LineRecord; +import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.JsonUtil; import java.io.IOException; @@ -13,15 +15,14 @@ public class HttpSyncStreamSender extends HttpSender { @Override - public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class responseClass, - Class genericClass) { + public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, ReturnType returnType) { try { var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofLines()); throwExceptionIfErrorIsPresent(httpResponse, Stream.class); - final var lineRecord = new CleverClientSSE.LineRecord(); + final var lineRecord = new LineRecord(); return httpResponse.body() .map(line -> { @@ -30,7 +31,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, return new CleverClientSSE(lineRecord); }) .filter(CleverClientSSE::isActualData) - .map(item -> JsonUtil.jsonToObject(item.getActualData(), responseClass)); + .map(item -> JsonUtil.jsonToObject(item.getActualData(), returnType.getBaseClass())); } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); 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 b82c993..a8d37d5 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientSSE.java @@ -1,27 +1,32 @@ package io.github.sashirestela.cleverclient.support; import java.util.List; +import java.util.Set; public class CleverClientSSE { + public static final String EVENT_HEADER = "event: "; private static final String DATA_HEADER = "data: "; + private static final String SEPARATOR = ""; private LineRecord lineRecord; private List endsOfStream; - private List linesToCheck; + private Set eventsWithHeader; public CleverClientSSE(LineRecord lineRecord) { this.lineRecord = lineRecord; - this.linesToCheck = Configurator.one().getLinesToCheck(); this.endsOfStream = Configurator.one().getEndsOfStream(); + this.eventsWithHeader = Set.of(SEPARATOR); } - public LineRecord getLineRecord() { - return lineRecord; + public CleverClientSSE(LineRecord lineRecord, Set eventsWithHeader) { + this.lineRecord = lineRecord; + this.endsOfStream = Configurator.one().getEndsOfStream(); + this.eventsWithHeader = eventsWithHeader; } public boolean isActualData() { - return linesToCheck.contains(lineRecord.previous()) && lineRecord.current().startsWith(DATA_HEADER) + return eventsWithHeader.contains(lineRecord.previous()) && lineRecord.current().startsWith(DATA_HEADER) && endsOfStream.stream().anyMatch(eos -> !lineRecord.current().contains(eos)); } @@ -29,6 +34,10 @@ public String getActualData() { return lineRecord.current().replace(DATA_HEADER, "").strip(); } + public String getMatchedEvent() { + return eventsWithHeader.contains(lineRecord.previous()) ? lineRecord.previous() : null; + } + public static class LineRecord { private String currentLine; diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java b/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java index 65e9f77..b337357 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/Configurator.java @@ -5,33 +5,24 @@ import lombok.Singular; import java.util.List; -import java.util.stream.Collectors; public class Configurator { - private static final String EVENT_HEADER = "event: "; - private static final String SEPARATOR = ""; - private static Configurator configurator = new Configurator(); - @Getter - private List eventsToRead; @Getter private List endsOfStream; - private List linesToCheck; private boolean wasBuilt = false; private Configurator() { } @Builder - public Configurator(@Singular("eventToRead") List eventsToRead, - @Singular("endOfStream") List endsOfStream) { + public Configurator(@Singular("endOfStream") List endsOfStream) { if (configurator.wasBuilt) { return; } - configurator.eventsToRead = eventsToRead; configurator.endsOfStream = endsOfStream; configurator.wasBuilt = true; } @@ -43,15 +34,4 @@ public static Configurator one() { return configurator; } - public List getLinesToCheck() { - if (linesToCheck == null) { - linesToCheck = eventsToRead.stream() - .filter(etr -> !etr.isEmpty()) - .map(etr -> (EVENT_HEADER + etr)) - .collect(Collectors.toList()); - linesToCheck.add(SEPARATOR); - } - return linesToCheck; - } - } diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java b/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java index 3b1961f..53c0272 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java @@ -1,6 +1,11 @@ package io.github.sashirestela.cleverclient.support; +import io.github.sashirestela.cleverclient.annotation.StreamType; + import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class ReturnType { @@ -9,6 +14,7 @@ public class ReturnType { private static final String LIST = "java.util.List"; private static final String INPUTSTREAM = "java.io.InputStream"; private static final String STRING = "java.lang.String"; + private static final String OBJECT = "java.lang.Object"; private static final String REGEX = "[<>]"; private static final String JAVA_PCK = "java"; @@ -19,6 +25,7 @@ public class ReturnType { private int firstIndex; private int lastIndex; private int prevLastIndex; + private Map> classByEvent; public ReturnType(String fullClassName) { this.fullClassName = fullClassName; @@ -31,6 +38,22 @@ public ReturnType(String fullClassName) { public ReturnType(Method method) { this(method.getGenericReturnType().getTypeName()); + if (method.isAnnotationPresent(StreamType.List.class)) { + this.classByEvent = calculateClassByEvent( + method.getDeclaredAnnotationsByType(StreamType.List.class)[0].value()); + } else if (method.isAnnotationPresent(StreamType.class)) { + this.classByEvent = calculateClassByEvent( + new StreamType[] { method.getDeclaredAnnotation(StreamType.class) }); + } + } + + private Map> calculateClassByEvent(StreamType[] streamTypeList) { + Map> classByEvent = new ConcurrentHashMap<>(); + Arrays.stream(streamTypeList).forEach(streamType -> { + Arrays.stream(streamType.events()) + .forEach(event -> classByEvent.put(CleverClientSSE.EVENT_HEADER + event, streamType.type())); + }); + return classByEvent; } public String toString() { @@ -41,6 +64,10 @@ public String getFullClassName() { return fullClassName; } + public Map> getClassByEvent() { + return this.classByEvent; + } + public Class getBaseClass() { return getClass(lastIndex); } @@ -69,13 +96,17 @@ public Category category() { private Category asyncCategory() { if (isStream()) { - return Category.ASYNC_STREAM; + if (isObject()) { + return Category.ASYNC_STREAM_OBJECT; + } else { + return Category.ASYNC_STREAM; + } } else if (isList()) { return Category.ASYNC_LIST; } else if (isGeneric()) { return Category.ASYNC_GENERIC; - } else if (isObject()) { - return Category.ASYNC_OBJECT; + } else if (isCustom()) { + return Category.ASYNC_CUSTOM; } else if (isBinary()) { return Category.ASYNC_BINARY; } else if (isPlainText()) { @@ -87,13 +118,17 @@ private Category asyncCategory() { private Category syncCategory() { if (isStream()) { - return Category.SYNC_STREAM; + if (isObject()) { + return Category.SYNC_STREAM_OBJECT; + } else { + return Category.SYNC_STREAM; + } } else if (isList()) { return Category.SYNC_LIST; } else if (isGeneric()) { return Category.SYNC_GENERIC; - } else if (isObject()) { - return Category.SYNC_OBJECT; + } else if (isCustom()) { + return Category.SYNC_CUSTOM; } else if (isBinary()) { return Category.SYNC_BINARY; } else if (isPlainText()) { @@ -103,32 +138,32 @@ private Category syncCategory() { } } - public boolean isAsync() { + private boolean isAsync() { return size > 1 && ASYNC.equals(returnTypeArray[firstIndex]); } - public boolean isStream() { + private boolean isStream() { return size > 1 && STREAM.equals(returnTypeArray[prevLastIndex]); } - public boolean isList() { + private boolean isList() { return size > 1 && LIST.equals(returnTypeArray[prevLastIndex]); } - public boolean isGeneric() { + private boolean isGeneric() { return ((isAsync() && size > 2) || (!isAsync() && size > 1)) && !returnTypeArray[prevLastIndex].startsWith(JAVA_PCK); } - public boolean isObject() { - return !isInputStream() && !isString() && (size == 1 || (size == 2 && isAsync())); + private boolean isCustom() { + return !isInputStream() && !isString() && !isObject() && (size == 1 || (size == 2 && isAsync())); } - public boolean isBinary() { + private boolean isBinary() { return isInputStream() && (size == 1 || (size == 2 && isAsync())); } - public boolean isPlainText() { + private boolean isPlainText() { return isString() && (size == 1 || (size == 2 && isAsync())); } @@ -140,17 +175,23 @@ private boolean isString() { return STRING.equals(returnTypeArray[lastIndex]); } + private boolean isObject() { + return OBJECT.equals(returnTypeArray[lastIndex]); + } + public enum Category { + ASYNC_STREAM_OBJECT, ASYNC_STREAM, ASYNC_LIST, ASYNC_GENERIC, - ASYNC_OBJECT, + ASYNC_CUSTOM, ASYNC_BINARY, ASYNC_PLAIN_TEXT, + SYNC_STREAM_OBJECT, SYNC_STREAM, SYNC_LIST, SYNC_GENERIC, - SYNC_OBJECT, + SYNC_CUSTOM, SYNC_BINARY, SYNC_PLAIN_TEXT; } diff --git a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java index f515b79..4884ad7 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java @@ -1,6 +1,8 @@ package io.github.sashirestela.cleverclient.http; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.Configurator; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,6 +38,13 @@ class HttpProcessorTest { HttpResponse> httpResponseStream = mock(HttpResponse.class); HttpResponse httpResponseBinary = mock(HttpResponse.class); + @BeforeAll + static void setup() { + Configurator.builder() + .endOfStream("END") + .build(); + } + @BeforeEach void init() { httpProcessor = HttpProcessor.builder() @@ -198,6 +207,33 @@ void shouldThrownExceptionWhenMethodReturnTypeIsAStream() throws IOException, In assertThrows(CleverClientException.class, () -> service.getDemoStream(requestDemo)); } + @Test + void shouldReturnAStreamSyncWhenMethodReturnTypeIsAStreamObject() throws IOException, InterruptedException { + when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofLines().getClass()))) + .thenReturn(httpResponseStream); + when(httpResponseStream.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponseStream.body()) + .thenReturn(Stream.of("event: created", + "data: {\"id\":100,\"description\":\"Description\",\"active\":true}")); + + var service = httpProcessor.createProxy(ITest.SyncService.class); + var actualStreamObject = service.getStreamObject(new ITest.RequestDemo("Descr", null)); + var actualObject = actualStreamObject.findFirst().get(); + var expectedObject = new ITest.Demo(100, "Description", true); + + assertEquals(expectedObject, actualObject); + } + + @Test + void shouldThrownExceptionWhenMethodReturnTypeIsAStreamObject() throws IOException, InterruptedException { + when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofLines().getClass()))) + .thenThrow(new InterruptedException("The operation was interrupted")); + + var service = httpProcessor.createProxy(ITest.SyncService.class); + var requestDemo = new ITest.RequestDemo("Descr", null); + assertThrows(CleverClientException.class, () -> service.getStreamObject(requestDemo)); + } + @Test void shouldReturnAStringAsyncWhenMethodReturnTypeIsAString() { when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofString().getClass()))) @@ -288,6 +324,23 @@ void shouldReturnAStreamAsyncWhenMethodReturnTypeIsAStream() { assertEquals(expectedDemo, actualDemo); } + @Test + void shouldReturnAStreamAsyncWhenMethodReturnTypeIsAStreamObject() { + when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofLines().getClass()))) + .thenReturn(CompletableFuture.completedFuture(httpResponseStream)); + when(httpResponseStream.statusCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(httpResponseStream.body()) + .thenReturn(Stream.of("event: created", + "data: {\"id\":100,\"description\":\"Description\",\"active\":true}")); + + var service = httpProcessor.createProxy(ITest.AsyncService.class); + var actualStreamObject = service.getStreamObject(new ITest.RequestDemo("Descr", null)).join(); + var actualObject = actualStreamObject.findFirst().get(); + var expectedObject = new ITest.Demo(100, "Description", true); + + assertEquals(expectedObject, actualObject); + } + @Test void shouldReturnAnObjectWhenMethodIsAnnotatedWithMultipart() { when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofString().getClass()))) diff --git a/src/test/java/io/github/sashirestela/cleverclient/http/ITest.java b/src/test/java/io/github/sashirestela/cleverclient/http/ITest.java index a926dae..a0daedd 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/http/ITest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/http/ITest.java @@ -8,6 +8,7 @@ import io.github.sashirestela.cleverclient.annotation.Path; import io.github.sashirestela.cleverclient.annotation.Query; import io.github.sashirestela.cleverclient.annotation.Resource; +import io.github.sashirestela.cleverclient.annotation.StreamType; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -82,6 +83,11 @@ interface AsyncService { @POST CompletableFuture> getDemoStream(@Body RequestDemo request); + @POST + @StreamType(type = Demo.class, events = { "created", "completed" }) + @StreamType(type = String.class, events = { "delta" }) + CompletableFuture> getStreamObject(@Body RequestDemo request); + @Multipart @POST CompletableFuture getFile(@Body RequestDemo request); @@ -114,6 +120,10 @@ interface SyncService { @POST Stream getDemoStream(@Body RequestDemo request); + @POST + @StreamType(type = Demo.class, events = { "created" }) + Stream getStreamObject(@Body RequestDemo request); + } interface NotSavedService { 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 46bb40c..924efb0 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/support/CleverClientSSETest.java @@ -4,29 +4,32 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Set; + import static org.junit.jupiter.api.Assertions.assertEquals; class CleverClientSSETest { + Set events = Set.of("event: process", "event: process2"); + @BeforeAll static void setup() { Configurator.builder() - .eventToRead("process") .endOfStream("END") .build(); } @Test - void shouldReturnExpectedValueWhenRawDataHasDifferentValues() { + void shouldReturnExpectedValueWhenRawDataMeetsConditions() { Object[][] testData = { - { 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("event: process", "data: Actual data."), events), true }, + { new CleverClientSSE(new LineRecord("", "data: Actual data.")), true }, + { new CleverClientSSE(new LineRecord("event: other", "data: Actual data."), events), false }, + { new CleverClientSSE(new LineRecord("event: process", "data : Actual data."), events), false }, + { new CleverClientSSE(new LineRecord("", "data : 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("event: process", "data: END"), events), false }, { new CleverClientSSE(new LineRecord("", "data: END")), false } }; for (Object[] data : testData) { @@ -38,14 +41,29 @@ void shouldReturnExpectedValueWhenRawDataHasDifferentValues() { } @Test - @SuppressWarnings("unused") - void shouldReturnTheActualDataWhenRawDataMeetsConditions() { + void shouldReturnCleanDataWhenRawDataMeetsConditions() { CleverClientSSE event = new CleverClientSSE( new LineRecord("event: process", "data: This is the actual data. ")); - var rawData = event.getLineRecord(); var actualData = event.getActualData(); var expectedData = "This is the actual data."; assertEquals(expectedData, actualData); } + @Test + void shouldReturnExpectedMatcheEventWhenRawDataMeetsConditions() { + Object[][] testData = { + { new CleverClientSSE(new LineRecord("event: process", "data: Actual data."), events), + "event: process" }, + { new CleverClientSSE(new LineRecord("event: other", "data: Actual data."), events), null }, + { new CleverClientSSE(new LineRecord("", "data: Actual data.")), "" } + }; + for (Object[] data : testData) { + var event = (CleverClientSSE) data[0]; + var actualMatchedEvent = event.getMatchedEvent(); + var expectedMatchedEvent = (String) data[1]; + assertEquals(expectedMatchedEvent, actualMatchedEvent); + } + + } + } diff --git a/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java b/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java index 2a0c347..395b31e 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java @@ -1,5 +1,6 @@ package io.github.sashirestela.cleverclient.support; +import io.github.sashirestela.cleverclient.annotation.StreamType; import org.junit.jupiter.api.Test; import java.io.InputStream; @@ -7,10 +8,12 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class ReturnTypeTest { @@ -19,23 +22,25 @@ void shouldReturnBaseClassForAMethod() throws NoSuchMethodException, SecurityExc var method = TestInterface.class.getMethod("asyncStreamMethod", new Class[] {}); var returnType = new ReturnType(method); var actualClass = returnType.getBaseClass(); - var expectedClass = Object.class; + var expectedClass = MyClass.class; assertEquals(expectedClass, actualClass); } @Test void shouldReturnCategoryAccordingToTheMethodType() throws NoSuchMethodException, SecurityException { var testData = Map.ofEntries( + Map.entry("asyncStreamObjectMethod", ReturnType.Category.ASYNC_STREAM_OBJECT), Map.entry("asyncStreamMethod", ReturnType.Category.ASYNC_STREAM), Map.entry("asyncListMethod", ReturnType.Category.ASYNC_LIST), Map.entry("asyncGenericMethod", ReturnType.Category.ASYNC_GENERIC), - Map.entry("asyncObjectMethod", ReturnType.Category.ASYNC_OBJECT), + Map.entry("asyncMyClassMethod", ReturnType.Category.ASYNC_CUSTOM), Map.entry("asyncBinaryMethod", ReturnType.Category.ASYNC_BINARY), Map.entry("asyncStringMethod", ReturnType.Category.ASYNC_PLAIN_TEXT), + Map.entry("syncStreamObjectMethod", ReturnType.Category.SYNC_STREAM_OBJECT), Map.entry("syncStreamMethod", ReturnType.Category.SYNC_STREAM), Map.entry("syncListMethod", ReturnType.Category.SYNC_LIST), Map.entry("syncGenericMethod", ReturnType.Category.SYNC_GENERIC), - Map.entry("syncObjectMethod", ReturnType.Category.SYNC_OBJECT), + Map.entry("syncMyClassMethod", ReturnType.Category.SYNC_CUSTOM), Map.entry("syncBinaryMethod", ReturnType.Category.SYNC_BINARY), Map.entry("syncStringMethod", ReturnType.Category.SYNC_PLAIN_TEXT)); for (String methodName : testData.keySet()) { @@ -57,40 +62,67 @@ void shouldReturnNullCategoryWhenMethodReturnTypeIsNotExpected() throws NoSuchMe assertNull(returnType.category()); } + @Test + void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType() + throws NoSuchMethodException, SecurityException { + var method = TestInterface.class.getMethod("asyncStreamObjectMethod", new Class[] {}); + var returnType = new ReturnType(method); + var actualMap = returnType.getClassByEvent(); + var expectedMap = new ConcurrentHashMap<>(); + expectedMap.put(CleverClientSSE.EVENT_HEADER + "first.create", First.class); + expectedMap.put(CleverClientSSE.EVENT_HEADER + "first.complete", First.class); + expectedMap.put(CleverClientSSE.EVENT_HEADER + "second.create", Second.class); + assertTrue(expectedMap.equals(actualMap)); + } + static interface TestInterface { - CompletableFuture> asyncStreamMethod(); + @StreamType(type = First.class, events = { "first.create", "first.complete" }) + @StreamType(type = Second.class, events = { "second.create" }) + CompletableFuture> asyncStreamObjectMethod(); - CompletableFuture> asyncListMethod(); + CompletableFuture> asyncStreamMethod(); - CompletableFuture> asyncGenericMethod(); + CompletableFuture> asyncListMethod(); - CompletableFuture asyncObjectMethod(); + CompletableFuture> asyncGenericMethod(); + + CompletableFuture asyncMyClassMethod(); CompletableFuture asyncBinaryMethod(); CompletableFuture asyncStringMethod(); - CompletableFuture> asyncSetMethod(); + CompletableFuture> asyncSetMethod(); + + Stream syncStreamObjectMethod(); - Stream syncStreamMethod(); + Stream syncStreamMethod(); - List syncListMethod(); + List syncListMethod(); - Generic syncGenericMethod(); + Generic syncGenericMethod(); - Object syncObjectMethod(); + MyClass syncMyClassMethod(); InputStream syncBinaryMethod(); String syncStringMethod(); - Set syncSetMethod(); + Set syncSetMethod(); } - static interface Generic { + static class First { + } + + static class Second { + } + static class MyClass { + } + + static interface Generic { } }