From 018670194c41acd43412042d0f53ebdf1c79aa87 Mon Sep 17 00:00:00 2001 From: Sashir Estela Date: Sat, 23 Mar 2024 00:45:02 +0000 Subject: [PATCH 1/4] Allow composite of StreamType annotations --- .../sashirestela/cleverclient/Event.java | 3 +++ .../cleverclient/annotation/StreamType.java | 4 +-- .../cleverclient/support/ReturnType.java | 25 ++++++++++++++++++- .../cleverclient/support/CompositeOne.java | 15 +++++++++++ .../cleverclient/support/CompositeTwo.java | 17 +++++++++++++ .../cleverclient/support/ReturnTypeTest.java | 19 +++++++++++--- 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/test/java/io/github/sashirestela/cleverclient/support/CompositeOne.java create mode 100644 src/test/java/io/github/sashirestela/cleverclient/support/CompositeTwo.java diff --git a/src/main/java/io/github/sashirestela/cleverclient/Event.java b/src/main/java/io/github/sashirestela/cleverclient/Event.java index 30d865f..6c0d9fc 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/Event.java +++ b/src/main/java/io/github/sashirestela/cleverclient/Event.java @@ -3,6 +3,9 @@ import lombok.Builder; import lombok.Value; +/** + * Represents every event in a Server Sent Event interaction. + */ @Value @Builder public class Event { diff --git a/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java b/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java index c78e0a9..3403977 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java +++ b/src/main/java/io/github/sashirestela/cleverclient/annotation/StreamType.java @@ -11,7 +11,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Repeatable(List.class) public @interface StreamType { @@ -21,7 +21,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) + @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @interface List { StreamType[] value(); 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 0ba7b30..30dbb9c 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java @@ -2,6 +2,7 @@ import io.github.sashirestela.cleverclient.annotation.StreamType; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Map; @@ -44,13 +45,35 @@ public ReturnType(Method method) { private void setClassByEventIfExists(Method method) { if (method.isAnnotationPresent(StreamType.List.class)) { this.classByEvent = calculateClassByEvent( - method.getDeclaredAnnotationsByType(StreamType.List.class)[0].value()); + method.getDeclaredAnnotation(StreamType.List.class).value()); } else if (method.isAnnotationPresent(StreamType.class)) { this.classByEvent = calculateClassByEvent( new StreamType[] { method.getDeclaredAnnotation(StreamType.class) }); + } else { + var annotations = method.getDeclaredAnnotations(); + if (isAnnotationPresent(annotations, StreamType.List.class)) { + this.classByEvent = calculateClassByEvent( + Arrays.stream(annotations) + .map(a -> a.annotationType().getDeclaredAnnotation(StreamType.List.class).value()) + .findFirst() + .get()); + } else if (isAnnotationPresent(annotations, StreamType.class)) { + this.classByEvent = calculateClassByEvent( + new StreamType[] { Arrays.stream(annotations) + .map(a -> a.annotationType().getDeclaredAnnotation(StreamType.class)) + .findFirst() + .get() }); + } } } + private boolean isAnnotationPresent(Annotation[] annotations, Class clazz) { + return Arrays.stream(annotations) + .filter(a -> a.annotationType().isAnnotationPresent(clazz)) + .findFirst() + .isPresent(); + } + private Map> calculateClassByEvent(StreamType[] streamTypeList) { Map> map = new ConcurrentHashMap<>(); Arrays.stream(streamTypeList).forEach(streamType -> { diff --git a/src/test/java/io/github/sashirestela/cleverclient/support/CompositeOne.java b/src/test/java/io/github/sashirestela/cleverclient/support/CompositeOne.java new file mode 100644 index 0000000..725e4aa --- /dev/null +++ b/src/test/java/io/github/sashirestela/cleverclient/support/CompositeOne.java @@ -0,0 +1,15 @@ +package io.github.sashirestela.cleverclient.support; + +import io.github.sashirestela.cleverclient.annotation.StreamType; +import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@StreamType(type = First.class, events = { "first.create", "first.complete" }) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CompositeOne { +} diff --git a/src/test/java/io/github/sashirestela/cleverclient/support/CompositeTwo.java b/src/test/java/io/github/sashirestela/cleverclient/support/CompositeTwo.java new file mode 100644 index 0000000..e151dd5 --- /dev/null +++ b/src/test/java/io/github/sashirestela/cleverclient/support/CompositeTwo.java @@ -0,0 +1,17 @@ +package io.github.sashirestela.cleverclient.support; + +import io.github.sashirestela.cleverclient.annotation.StreamType; +import io.github.sashirestela.cleverclient.support.ReturnTypeTest.First; +import io.github.sashirestela.cleverclient.support.ReturnTypeTest.Second; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@StreamType(type = First.class, events = { "first.create", "first.complete" }) +@StreamType(type = Second.class, events = { "second.create" }) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CompositeTwo { +} 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 4f9a7b0..e16c983 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/support/ReturnTypeTest.java @@ -1,7 +1,6 @@ package io.github.sashirestela.cleverclient.support; import io.github.sashirestela.cleverclient.Event; -import io.github.sashirestela.cleverclient.annotation.StreamType; import org.junit.jupiter.api.Test; import java.io.InputStream; @@ -63,7 +62,7 @@ void shouldReturnNullCategoryWhenMethodReturnTypeIsNotExpected() throws NoSuchMe } @Test - void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType() + void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeMultiStreamType() throws NoSuchMethodException, SecurityException { var method = TestInterface.class.getMethod("asyncStreamEventMethod", new Class[] {}); var returnType = new ReturnType(method); @@ -75,10 +74,21 @@ void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithStreamType() assertEquals(Boolean.TRUE, expectedMap.equals(actualMap)); } + @Test + void shouldReturnMapClassByEventWhenTheMethodIsAnnotatedWithCompositeSingleStreamType() + throws NoSuchMethodException, SecurityException { + var method = TestInterface.class.getMethod("syncStreamEventMethod", new Class[] {}); + var returnType = new ReturnType(method); + var actualMap = returnType.getClassByEvent(); + var expectedMap = new ConcurrentHashMap<>(); + expectedMap.put("first.create", First.class); + expectedMap.put("first.complete", First.class); + assertEquals(Boolean.TRUE, expectedMap.equals(actualMap)); + } + static interface TestInterface { - @StreamType(type = First.class, events = { "first.create", "first.complete" }) - @StreamType(type = Second.class, events = { "second.create" }) + @CompositeTwo CompletableFuture> asyncStreamEventMethod(); CompletableFuture> asyncStreamMethod(); @@ -95,6 +105,7 @@ static interface TestInterface { CompletableFuture> asyncSetMethod(); + @CompositeOne Stream syncStreamEventMethod(); Stream syncStreamMethod(); From 8ed1b9f756c99ec4a05d08afa711108a0b8eff88 Mon Sep 17 00:00:00 2001 From: Sashir Estela Date: Sat, 23 Mar 2024 00:54:17 +0000 Subject: [PATCH 2/4] Update Readme --- README.md | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b752b2c..2598aef 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Library that makes it easy to use the Java HttpClient to perform http operations 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 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```: +For example, if we want to use the public API [JsonPlaceHolder](https://jsonplaceholder.typicode.com/) and call its endpoint ```/posts```, we just have to create an entity ```Post```, an interface ```PostService``` with special annotatons, and call the API through ```CleverClient```: ```java // Entity @@ -152,27 +152,29 @@ var cleverClient = CleverClient.builder() ### Interface Annotations -| 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 | class type 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 | +| 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 | +| PATCH | Method | PATCH endpoint's url | optional | One | +| Multipart | Method | (None) | none | One | +| StreamType | Method | Class type and events array | mandatory both | Many | +| StreamType | Annotation | Class type 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 [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java). Commonly you will use more than one to indicate what class (type) is related to what events (array of Strings). +* ```StreamType``` is used with methods whose return type is Stream of [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java). Tipically you will use more than one of this annotation to indicate what classes (types) are related to what events (array of Strings). You can also use them for custom annotations in case you want to reuse them for many methods, so you just apply the custom composite annotation. * ```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. @@ -180,9 +182,9 @@ 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, 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 [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java), 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). +The reponse types are determined from the method's return types. 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 [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java), 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 | +| Response Type | Sync/Async | Description | |------------------------------------|------------|-----------------------------| | CompletableFuture> | Async | SSE (*) as Stream of type T | | Stream\ | Sync | SSE (*) as Stream of type T | @@ -203,7 +205,7 @@ The reponse types are determined from the method responses. We don't need any an * ```CompletableFuture>``` and ```Stream``` are used for handling SSE without events and data of the class ```T``` only. * ```CompletableFuture>``` and ```Stream``` are used for handling SSE with multiple events and data of different classes. -* The [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java) class will bring the event name and the data object. +* The [Event](./src/main/java/io/github/sashirestela/cleverclient/Event.java) class will bring for each event: the event name and the data object. ### Interface Default Methods From b83b08b433393b00951c042aa208f341d7a68e24 Mon Sep 17 00:00:00 2001 From: Sashir Estela Date: Sat, 23 Mar 2024 00:55:04 +0000 Subject: [PATCH 3/4] Deploy release 1.3.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0576368..1900792 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.sashirestela cleverclient - 1.3.1 + 1.3.2 jar cleverclient From 7b0a775e6b0be04c2d7ba8ce078d656fa8ca931e Mon Sep 17 00:00:00 2001 From: Sashir Estela Date: Sat, 23 Mar 2024 01:07:18 +0000 Subject: [PATCH 4/4] Fixing Sonar code smells --- .../sashirestela/cleverclient/support/ReturnType.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 30dbb9c..77736ec 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/ReturnType.java @@ -56,22 +56,19 @@ private void setClassByEventIfExists(Method method) { Arrays.stream(annotations) .map(a -> a.annotationType().getDeclaredAnnotation(StreamType.List.class).value()) .findFirst() - .get()); + .orElse(null)); } else if (isAnnotationPresent(annotations, StreamType.class)) { this.classByEvent = calculateClassByEvent( new StreamType[] { Arrays.stream(annotations) .map(a -> a.annotationType().getDeclaredAnnotation(StreamType.class)) .findFirst() - .get() }); + .orElse(null) }); } } } private boolean isAnnotationPresent(Annotation[] annotations, Class clazz) { - return Arrays.stream(annotations) - .filter(a -> a.annotationType().isAnnotationPresent(clazz)) - .findFirst() - .isPresent(); + return Arrays.stream(annotations).anyMatch(a -> a.annotationType().isAnnotationPresent(clazz)); } private Map> calculateClassByEvent(StreamType[] streamTypeList) {