diff --git a/dynamic-proxy/README.md b/dynamic-proxy/README.md new file mode 100644 index 000000000..21976ddeb --- /dev/null +++ b/dynamic-proxy/README.md @@ -0,0 +1,354 @@ +--- +title: Dynamic Proxy +category: Structural +language: en +tag: +- Generic +--- + +## Intent +Dynamic proxy is a Java mechanism that allows developers to create a proxy instance for interfaces at runtime. It is primarily used for intercepting method calls, enabling developers to add additional processing around the actual method invocation. + +## Explanation + +### Real-world example +Mockito, a popular Java mocking framework, employs dynamic proxy to create mock objects for testing. Mock objects mimic the behavior of real objects, allowing developers to isolate components and verify interactions in unit tests. + +Consider a scenario where a service class depends on an external component, such as a database access object (DAO). Instead of interacting with a real DAO in a test, Mockito can dynamically generate a proxy, intercepting method invocations and returning predefined values. This enables focused unit testing without the need for a real database connection. + +### In plain words +Dynamic proxy is a specialized form of proxy in Java, serving as a flexible and dynamic method to intercept and manipulate method calls. By utilizing dynamic proxies, developers can implement additional functionalities without modifying the original class code. + +### Wikipedia says +A dynamic proxy class is a class that implements a list of interfaces specified at runtime such that a method invocation through one of the interfaces on an instance of the class will be encoded and dispatched to another object through a uniform interface. Thus, a dynamic proxy class can be used to create a type-safe proxy object for a list of interfaces without requiring pre-generation of the proxy class, such as with compile-time tools. Method invocations on an instance of a dynamic proxy class are dispatched to a single method in the instance's invocation handler, and they are encoded with a _java.lang.reflect.Method_ object identifying the method that was invoked and an array of type _Object_ containing the arguments. + +### Programmatic Example +This example allow us to hit the public fake API https://jsonplaceholder.typicode.com for the resource Album through an interface. + +The call to _Proxy.newProxyInstance_ creates a new dynamic proxy for the _AlbumService_ interface and sets the _AlbumInvocationHandler_ class as the handler to intercept all the interface's methods. Everytime that we call an _AlbumService_'s method, the handler's method `invoke` will be call automatically, and it will pass all the method's metadata and arguments to other specialized class - _TinyRestClient_ - to prepare the Rest API call accordingly. + +![Sequence diagram](./etc/dynamic-proxy-sequence.png "Dynamic Proxy sequence diagram") + +In this demo, the Dynamic Proxy pattern help us to run business logic through an interface without an explicit implementation of that interface and supported on the Java Reflection approach. + +```java +public class App { + + private static final Logger logger = LoggerFactory.getLogger(App.class); + + static final String REST_API_URL = "https://jsonplaceholder.typicode.com"; + + private String baseUrl; + private HttpClient httpClient; + private AlbumService albumServiceProxy; + + /** + * Class constructor. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public App(String baseUrl, HttpClient httpClient) { + this.baseUrl = baseUrl; + this.httpClient = httpClient; + } + + /** + * Create the Dynamic Proxy linked to the AlbumService interface and to the AlbumInvocationHandler. + */ + public void createDynamicProxy() { + AlbumInvocationHandler albumInvocationHandler = new AlbumInvocationHandler(baseUrl, httpClient); + + albumServiceProxy = (AlbumService) Proxy.newProxyInstance( + App.class.getClassLoader(), new Class[] { AlbumService.class }, albumInvocationHandler); + } + + /** + * Call the methods of the Dynamic Proxy, in other words, the AlbumService interface's methods + * and receive the responses from the Rest API. + */ + public void callMethods() { + int albumId = 17; + int userId = 3; + + var albums = albumServiceProxy.readAlbums(); + albums.forEach(album -> logger.info("{}", album)); + + var album = albumServiceProxy.readAlbum(albumId); + logger.info("{}", album); + + var newAlbum = albumServiceProxy.createAlbum(Album.builder() + .title("Big World").userId(userId).build()); + logger.info("{}", newAlbum); + + var editAlbum = albumServiceProxy.updateAlbum(albumId, Album.builder() + .title("Green Valley").userId(userId).build()); + logger.info("{}", editAlbum); + + var removedAlbum = albumServiceProxy.deleteAlbum(albumId); + logger.info("{}", removedAlbum); + } + + /** + * Application entry point. + * + * @param args External arguments to be passed. Optional. + */ + public static void main(String[] args) { + App app = new App(App.REST_API_URL, HttpClient.newHttpClient()); + app.createDynamicProxy(); + app.callMethods(); + } + +} +``` + +Declaration of the AlbumService interface. Annotations were used to provide additional information such as: http method (Get, Post, Put, Delete), endpoint's url, path parameters, body parameters, among others. + +```java +public interface AlbumService { + + /** + * Get a list of albums from an endpoint. + * + * @return List of albums' data. + */ + @Get("/albums") + List readAlbums(); + + /** + * Get a specific album from an endpoint. + * + * @param albumId Album's id to search for. + * @return Album's data. + */ + @Get("/albums/{albumId}") + Album readAlbum(@Path("albumId") Integer albumId); + + /** + * Creates a new album. + * + * @param album Album's data to be created. + * @return New album's data. + */ + @Post("/albums") + Album createAlbum(@Body Album album); + + /** + * Updates an existing album. + * + * @param albumId Album's id to be modified. + * @param album New album's data. + * @return Updated album's data. + */ + @Put("/albums/{albumId}") + Album updateAlbum(@Path("albumId") Integer albumId, @Body Album album); + + /** + * Deletes an album. + * + * @param albumId Album's id to be deleted. + * @return Empty album. + */ + @Delete("/albums/{albumId}") + Album deleteAlbum(@Path("albumId") Integer albumId); + +} +``` + +Declaration of the AlbumInvocationHandler class whose method _invoke_ will be called automatically every time that an AlbumService's method is called. The call processing is delegated to the _TinyRestClient_. + +```java +public class AlbumInvocationHandler implements InvocationHandler { + + private static final Logger logger = LoggerFactory.getLogger(AlbumInvocationHandler.class); + + private TinyRestClient restClient; + + /** + * Class constructor. It instantiates a TinyRestClient object. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public AlbumInvocationHandler(String baseUrl, HttpClient httpClient) { + this.restClient = new TinyRestClient(baseUrl, httpClient); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + logger.info("===== Calling the method {}.{}()", + method.getDeclaringClass().getSimpleName(), method.getName()); + + return restClient.send(method, args); + } + +} +``` + +The TinyRestClient class handle all the http communication with a Rest API. It is supported by the built-in component HttpClient. This class is an optional part of the pattern. + +```java +public class TinyRestClient { + + private String baseUrl; + private HttpClient httpClient; + + /** + * Class constructor. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public TinyRestClient(String baseUrl, HttpClient httpClient) { + this.baseUrl = baseUrl; + this.httpClient = httpClient; + } + + /** + * Creates a http communication to request and receive data from an endpoint. + * + * @param method Interface's method which is annotated with a http method. + * @param args Method's arguments passed in the call. + * @return Response from the endpoint. + * @throws IOException Exception thrown when any fail happens in the call. + * @throws InterruptedException Exception thrown when call is interrupted. + */ + public Object send(Method method, Object[] args) throws IOException, InterruptedException { + var httpMethodAnnotation = getHttpMethodAnnotation(method.getDeclaredAnnotations()); + if (httpMethodAnnotation == null) { + return null; + } + var httpMethodName = httpMethodAnnotation.annotationType().getSimpleName().toUpperCase(); + var url = baseUrl + buildUrl(method, args, httpMethodAnnotation); + var bodyPublisher = buildBodyPublisher(method, args); + var httpRequest = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .method(httpMethodName, bodyPublisher) + .build(); + var httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + return getResponse(method, httpResponse); + } + + private String buildUrl(Method method, Object[] args, Annotation httpMethodAnnotation) { + var url = annotationValue(httpMethodAnnotation); + if (url == null) { + return ""; + } + var index = 0; + for (var parameter : method.getParameters()) { + var pathAnnotation = getAnnotationOf(parameter.getDeclaredAnnotations(), Path.class); + if (pathAnnotation != null) { + var pathParam = "{" + annotationValue(pathAnnotation) + "}"; + var pathValue = args[index].toString(); + url = url.replace(pathParam, pathValue); + } + index++; + } + return url; + } + + private HttpRequest.BodyPublisher buildBodyPublisher(Method method, Object[] args) { + var index = 0; + for (var parameter : method.getParameters()) { + var bodyAnnotation = getAnnotationOf(parameter.getDeclaredAnnotations(), Body.class); + if (bodyAnnotation != null) { + var body = JsonUtil.objectToJson(args[index]); + return HttpRequest.BodyPublishers.ofString(body); + } + index++; + } + return HttpRequest.BodyPublishers.noBody(); + } + + private Object getResponse(Method method, HttpResponse httpResponse) { + var rawData = httpResponse.body(); + var returnType = method.getGenericReturnType(); + if (returnType instanceof ParameterizedType) { + Class responseClass = (Class) (((ParameterizedType) returnType) + .getActualTypeArguments()[0]); + return JsonUtil.jsonToList(rawData, responseClass); + } else { + Class responseClass = method.getReturnType(); + return JsonUtil.jsonToObject(rawData, responseClass); + } + } + + private Annotation getHttpMethodAnnotation(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(annot -> annot.annotationType().isAnnotationPresent(HttpMethod.class)) + .findFirst().orElse(null); + } + + private Annotation getAnnotationOf(Annotation[] annotations, Class clazz) { + return Arrays.stream(annotations) + .filter(annot -> annot.annotationType().equals(clazz)) + .findFirst().orElse(null); + } + + private String annotationValue(Annotation annotation) { + var valueMethod = Arrays.stream(annotation.annotationType().getDeclaredMethods()) + .filter(methodAnnot -> methodAnnot.getName().equals("value")) + .findFirst().orElse(null); + if (valueMethod == null) { + return null; + } + Object result; + try { + result = valueMethod.invoke(annotation, (Object[]) null); + } catch (Exception e) { + result = null; + } + return (result instanceof String strResult ? strResult : null); + } +} +``` + +## Class diagram +![Class diagram](./etc/dynamic-proxy-classes.png "Dynamic Proxy class diagram") + +## Applicability +Dynamic proxy should be used when you need to augment or enhance your current functionality without modifying your current code. Some examples of that usage could be: + +- _Logging and Monitoring_: By creating a logging proxy, developers can intercept method invocations and log relevant information like method names, parameters, and execution times. + +- _Security and Access Control_: Developers can create proxies that enforce security policies, restricting access to certain methods based on user roles or permissions. + +- _Aspect-Oriented Programming_: Dynamic proxies facilitate the application of transversal aspects to methods without changing the original code. + +## Tutorials +- [Dynamic Proxies in Java - CodeGym](https://codegym.cc/groups/posts/208-dynamic-proxies) +- [Introduction To Java Dynamic Proxy - Xperti](https://xperti.io/blogs/java-dynamic-proxies-introduction/) + +## Known uses +Many frameworks and libraries use dynamic proxy to implement their functionalities: +- [Spring Framework](https://docs.spring.io/spring-framework/reference/core/aop.html), for aspect oriented programming. +- [Hibernate](https://hibernate.org/orm/), for data lazy loading. +- [Mockito](https://site.mockito.org/), for mocking objects in testing. +- [Cleverclient](https://github.com/sashirestela/cleverclient), for calling http endpoints through annotated interfaces. + +## Consequences + +### Pros +- **Increased flexibility in code**: Dynamic proxies in Java offer a high degree of flexibility, allowing developers to create versatile and adaptable applications. By using dynamic proxies, software +engineers can modify the behavior of methods at runtime, which is particularly useful in scenarios where the behavior of classes needs to be augmented or manipulated without altering their source code. +This flexibility is crucial in developing applications that require dynamic response to varying conditions or need to integrate with systems where interfaces may change over time. +- **Simplifying complex operations**: Dynamic proxies excel in simplifying complex operations, particularly in the areas of cross-cutting concerns such as logging, transaction management, and security. By intercepting method calls, dynamic proxies can uniformly apply certain operations across various methods and classes, thereby reducing the need for repetitive code. This capability is particularly beneficial in large-scale applications where such cross-cutting concerns are prevalent. For example, adding logging or authorization checks across multiple methods becomes a matter of implementing these features once in an invocation handler, rather than modifying each method individually. +- **Enhancing code maintainability**: Maintainability is a key advantage of using dynamic proxies. They promote cleaner and more organized code by separating the core business logic from cross-cutting concerns. This separation of concerns not only makes the codebase more understandable but also easier to test and debug. When the business logic is decoupled from aspects like logging or transaction handling, any changes in these areas do not impact the core functionality of the application. As a result, applications become more robust and easier to maintain and update, which is crucial in the fast-paced environment of software development where requirements and technologies are constantly evolving. + +### Cons +- **Performance overhead**: The use of reflection and method invocation through proxies can introduce latency, especially in performance-critical applications. This overhead might be negligible in most cases, but it becomes significant in scenarios with high-frequency method calls. +- **Complexity of debugging**: Since dynamic proxies introduce an additional layer of abstraction, tracing and debugging issues can be more challenging. It can be difficult to trace the flow of execution through proxies, especially when multiple proxies are involved. +- **Limited to interface-based programming**: They can only proxy interfaces, not classes. This limitation requires careful design considerations, particularly in situations where class-based proxies would be more appropriate. +- **Higher level of expertise**: Developers are normally not a fan of “magic code” — code that works in a non-transparent or overly complex manner. Those unfamiliar with the proxy pattern or reflection might find the codebase more complex to understand and maintain, potentially leading to errors or misuse of the feature. This complexity can be perceived as a form of “magic” that obscures the underlying process, making the code less intuitive and more challenging to debug or extend. Therefore, while dynamic proxies are powerful, their use should be approached with caution and a thorough understanding of their inner workings. + +## Related patterns +- [Proxy](https://java-design-patterns.com/patterns/proxy) + +## Credits +- [Dynamic Proxies in Java - Baeldung](https://www.baeldung.com/java-dynamic-proxies) +- [Dynamic Proxy Classes - Oracle](https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html) +- [Intro To Java Dynamic Proxies - KapreSoft](https://www.kapresoft.com/java/2023/12/27/intro-to-java-proxies.html) +- [Exploring the Depths of Dynamic Proxy in Java: A Comprehensive Guide](https://naveen-metta.medium.com/exploring-the-depths-of-dynamic-proxy-in-java-a-comprehensive-guide-f34fb45b38a3) diff --git a/dynamic-proxy/etc/dynamic-proxy-classes.png b/dynamic-proxy/etc/dynamic-proxy-classes.png new file mode 100644 index 000000000..86f5a7db7 Binary files /dev/null and b/dynamic-proxy/etc/dynamic-proxy-classes.png differ diff --git a/dynamic-proxy/etc/dynamic-proxy-classes.puml b/dynamic-proxy/etc/dynamic-proxy-classes.puml new file mode 100644 index 000000000..119f1dc11 --- /dev/null +++ b/dynamic-proxy/etc/dynamic-proxy-classes.puml @@ -0,0 +1,35 @@ +@startuml + +class A as "App" { +} + +interface I as "MyInterface" { + someMethod(args) + otherMethods(args) +} + +class P as "Proxy" <> { + {static} DynamicProxy newProxyInstance(\n classLoader,\n interfaces,\n invocationHandler\n) +} + +class D as "DynamicProxy" { + someMethod(args) + otherMethods(args) +} + +class H as "InvocationHandler" { + invoke(proxy, method, args) +} + +class X as "AuxiliarProcessor" { + process(method, args) +} + +A -r-> D + +I <|.. D : implements + +D -r-> H : delegate +H -r-> X : (optional) + +@enduml diff --git a/dynamic-proxy/etc/dynamic-proxy-sequence.png b/dynamic-proxy/etc/dynamic-proxy-sequence.png new file mode 100644 index 000000000..9a139b1c8 Binary files /dev/null and b/dynamic-proxy/etc/dynamic-proxy-sequence.png differ diff --git a/dynamic-proxy/etc/dynamic-proxy-sequence.puml b/dynamic-proxy/etc/dynamic-proxy-sequence.puml new file mode 100644 index 000000000..76705f81e --- /dev/null +++ b/dynamic-proxy/etc/dynamic-proxy-sequence.puml @@ -0,0 +1,63 @@ +@startuml + +participant App as A +participant Proxy as P <> +participant DynamicProxy as D <> +participant InvocationHandler as H +participant AuxiliarProcessor as R + +== Create Dynamic Proxy == + +create H +A -> H : new(args) +activate H + +create R +H -> R : new(args) +activate R + +R --> H : processor +deactivate R + +H --> A : invocationHandler +deactivate H + +||| + +A -> P : newProxyInstance(\n classLoader,\n new Class[]{MyInterface.class},\n invocationHandler\n) +activate P + +create D +P -> D : new +activate D + +D --> P : dynamicProxy +deactivate D + +P --> A : dynamicProxy +deactivate P + +== Call Interface's Method == + +A -> D : someMethod(args) +activate D + +D -> H : invoke(proxy, method, args) +activate H + +H -> R : process(method, args) +activate R + +R -> R : parse method's\nmetadata +R -> R : execute some\nprocessing + +R --> H : response +deactivate R + +H --> D : response +deactivate H + +D --> A : response +deactivate D + +@enduml diff --git a/dynamic-proxy/etc/dynamic-proxy.urm.puml b/dynamic-proxy/etc/dynamic-proxy.urm.puml new file mode 100644 index 000000000..5db603aae --- /dev/null +++ b/dynamic-proxy/etc/dynamic-proxy.urm.puml @@ -0,0 +1,96 @@ +@startuml +package com.iluwatar.dynamicproxy.tinyrestclient.annotation { + interface Body { + } + interface Delete { + + value() : String {abstract} + } + interface Get { + + value() : String {abstract} + } + interface HttpMethod { + } + interface Path { + + value() : String {abstract} + } + interface Post { + + value() : String {abstract} + } + interface Put { + + value() : String {abstract} + } +} +package com.iluwatar.dynamicproxy.tinyrestclient { + class JsonUtil { + - objectMapper : ObjectMapper {static} + - JsonUtil() + + jsonToList(json : String, clazz : Class) : List {static} + + jsonToObject(json : String, clazz : Class) : T {static} + + objectToJson(object : T) : String {static} + } + class TinyRestClient { + - baseUrl : String + + TinyRestClient(baseUrl : String) + - annotationValue(annotation : Annotation) : String + - buildBodyPublisher(method : Method, args : Object[]) : BodyPublisher + - buildUrl(method : Method, args : Object[], httpMethodAnnotation : Annotation) : String + - getAnnotationOf(annotations : Annotation[], clazz : Class) : Annotation + - getHttpMethodAnnotation(annotations : Annotation[]) : Annotation + - getResponse(method : Method, httpResponse : HttpResponse) : Object + + send(method : Method, args : Object[]) : Object + } +} +package com.iluwatar.dynamicproxy { + class Album { + - id : Integer + - title : String + - userId : Integer + + Album() + + Album(id : Integer, title : String, userId : Integer) + + builder() : AlbumBuilder {static} + # canEqual(other : Object) : boolean + + equals(o : Object) : boolean + + getId() : Integer + + getTitle() : String + + getUserId() : Integer + + hashCode() : int + + setId(id : Integer) + + setTitle(title : String) + + setUserId(userId : Integer) + + toString() : String + } + class AlbumBuilder { + - id : Integer + - title : String + - userId : Integer + ~ AlbumBuilder() + + build() : Album + + id(id : Integer) : AlbumBuilder + + title(title : String) : AlbumBuilder + + toString() : String + + userId(userId : Integer) : AlbumBuilder + } + class AlbumInvocationHandler { + - restClient : TinyRestClient + + AlbumInvocationHandler(baseUrl : String) + + invoke(proxy : Object, method : Method, args : Object[]) : Object + } + interface AlbumService { + + createAlbum(Album) : Album {abstract} + + deleteAlbum(Integer) : Album {abstract} + + readAlbum(Integer) : Album {abstract} + + readAlbums() : List {abstract} + + updateAlbum(Integer, Album) : Album {abstract} + } + class App { + ~ REST_API_URL : String {static} + - albumServiceProxy : AlbumService + + App() + + callMethods() + + createDynamicProxy() + + main(args : String[]) {static} + } +} +AlbumInvocationHandler --> "-restClient" TinyRestClient +App --> "-albumServiceProxy" AlbumService +@enduml \ No newline at end of file diff --git a/dynamic-proxy/pom.xml b/dynamic-proxy/pom.xml new file mode 100644 index 000000000..0ec69f0bc --- /dev/null +++ b/dynamic-proxy/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + dynamic-proxy + + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + org.springframework + spring-web + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.dynamicproxy.App + + + + + + + + + diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/Album.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/Album.java new file mode 100644 index 000000000..1a4efc606 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/Album.java @@ -0,0 +1,46 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * This class represents an endpoint resource that + * we are going to interchange with a Rest API server. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor(force = true) +@Builder +public class Album { + + private Integer id; + private String title; + private Integer userId; + +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumInvocationHandler.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumInvocationHandler.java new file mode 100644 index 000000000..e53485ae0 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumInvocationHandler.java @@ -0,0 +1,61 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy; + +import com.iluwatar.dynamicproxy.tinyrestclient.TinyRestClient; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.net.http.HttpClient; +import lombok.extern.slf4j.Slf4j; + +/** + * Class whose method 'invoke' will be called every time that an interface's method is called. + * That interface is linked to this class by the Proxy class. + */ +@Slf4j +public class AlbumInvocationHandler implements InvocationHandler { + + private TinyRestClient restClient; + + /** + * Class constructor. It instantiates a TinyRestClient object. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public AlbumInvocationHandler(String baseUrl, HttpClient httpClient) { + this.restClient = new TinyRestClient(baseUrl, httpClient); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + LOGGER.info("===== Calling the method {}.{}()", + method.getDeclaringClass().getSimpleName(), method.getName()); + + return restClient.send(method, args); + } + +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumService.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumService.java new file mode 100644 index 000000000..c0975b6e6 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/AlbumService.java @@ -0,0 +1,87 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy; + +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Body; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Delete; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Get; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Path; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Post; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Put; +import java.util.List; + +/** + * Every method in this interface is annotated with the necessary metadata to represents an endpoint + * that we can call to communicate with a host server which is serving a resource by Rest API. + * This interface is focused in the resource Album. + */ +public interface AlbumService { + + /** + * Get a list of albums from an endpoint. + * + * @return List of albums' data. + */ + @Get("/albums") + List readAlbums(); + + /** + * Get a specific album from an endpoint. + * + * @param albumId Album's id to search for. + * @return Album's data. + */ + @Get("/albums/{albumId}") + Album readAlbum(@Path("albumId") Integer albumId); + + /** + * Creates a new album. + * + * @param album Album's data to be created. + * @return New album's data. + */ + @Post("/albums") + Album createAlbum(@Body Album album); + + /** + * Updates an existing album. + * + * @param albumId Album's id to be modified. + * @param album New album's data. + * @return Updated album's data. + */ + @Put("/albums/{albumId}") + Album updateAlbum(@Path("albumId") Integer albumId, @Body Album album); + + /** + * Deletes an album. + * + * @param albumId Album's id to be deleted. + * @return Empty album. + */ + @Delete("/albums/{albumId}") + Album deleteAlbum(@Path("albumId") Integer albumId); + +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/App.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/App.java new file mode 100644 index 000000000..61118694d --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/App.java @@ -0,0 +1,109 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy; + +import java.lang.reflect.Proxy; +import java.net.http.HttpClient; +import lombok.extern.slf4j.Slf4j; + +/** + * Application to demonstrate the Dynamic Proxy pattern. This application allow us to hit the public + * fake API https://jsonplaceholder.typicode.com for the resource Album through an interface. + * The call to Proxy.newProxyInstance creates a new dynamic proxy for the AlbumService interface and + * sets the AlbumInvocationHandler class as the handler to intercept all the interface's methods. + * Everytime that we call an AlbumService's method, the handler's method "invoke" will be call + * automatically, and it will pass all the method's metadata and arguments to other specialized + * class - TinyRestClient - to prepare the Rest API call accordingly. + * In this demo, the Dynamic Proxy pattern help us to run business logic through interfaces without + * an explicit implementation of the interfaces and supported on the Java Reflection approach. + */ +@Slf4j +public class App { + + static final String REST_API_URL = "https://jsonplaceholder.typicode.com"; + + private String baseUrl; + private HttpClient httpClient; + private AlbumService albumServiceProxy; + + /** + * Class constructor. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public App(String baseUrl, HttpClient httpClient) { + this.baseUrl = baseUrl; + this.httpClient = httpClient; + } + + /** + * Application entry point. + * + * @param args External arguments to be passed. Optional. + */ + public static void main(String[] args) { + App app = new App(App.REST_API_URL, HttpClient.newHttpClient()); + app.createDynamicProxy(); + app.callMethods(); + } + + /** + * Create the Dynamic Proxy linked to the AlbumService interface and to the AlbumInvocationHandler. + */ + public void createDynamicProxy() { + AlbumInvocationHandler albumInvocationHandler = new AlbumInvocationHandler(baseUrl, httpClient); + + albumServiceProxy = (AlbumService) Proxy.newProxyInstance( + App.class.getClassLoader(), new Class[]{AlbumService.class}, albumInvocationHandler); + } + + /** + * Call the methods of the Dynamic Proxy, in other words, the AlbumService interface's methods + * and receive the responses from the Rest API. + */ + public void callMethods() { + int albumId = 17; + int userId = 3; + + var albums = albumServiceProxy.readAlbums(); + albums.forEach(album -> LOGGER.info("{}", album)); + + var album = albumServiceProxy.readAlbum(albumId); + LOGGER.info("{}", album); + + var newAlbum = albumServiceProxy.createAlbum(Album.builder() + .title("Big World").userId(userId).build()); + LOGGER.info("{}", newAlbum); + + var editAlbum = albumServiceProxy.updateAlbum(albumId, Album.builder() + .title("Green Valley").userId(userId).build()); + LOGGER.info("{}", editAlbum); + + var removedAlbum = albumServiceProxy.deleteAlbum(albumId); + LOGGER.info("{}", removedAlbum); + } + +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/JsonUtil.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/JsonUtil.java new file mode 100644 index 000000000..f76fe88ec --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/JsonUtil.java @@ -0,0 +1,98 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionType; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility class to handle Json operations. + */ +@Slf4j +public class JsonUtil { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + private JsonUtil() { + } + + /** + * Convert an object to a Json string representation. + * + * @param object Object to convert. + * @param Object's class. + * @return Json string. + */ + public static String objectToJson(T object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + LOGGER.error("Cannot convert the object " + object + " to Json.", e); + return null; + } + } + + /** + * Convert a Json string to an object of a class. + * + * @param json Json string to convert. + * @param clazz Object's class. + * @param Object's generic class. + * @return Object. + */ + public static T jsonToObject(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (IOException e) { + LOGGER.error("Cannot convert the Json " + json + " to class " + clazz.getName() + ".", e); + return null; + } + } + + /** + * Convert a Json string to a List of objects of a class. + * + * @param json Json string to convert. + * @param clazz Object's class. + * @param Object's generic class. + * @return List of objects. + */ + public static List jsonToList(String json, Class clazz) { + try { + CollectionType listType = objectMapper.getTypeFactory() + .constructCollectionType(ArrayList.class, clazz); + return objectMapper.reader().forType(listType).readValue(json); + } catch (JsonProcessingException e) { + LOGGER.error("Cannot convert the Json " + json + " to List of " + clazz.getName() + ".", e); + return List.of(); + } + } + +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/TinyRestClient.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/TinyRestClient.java new file mode 100644 index 000000000..854791a72 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/TinyRestClient.java @@ -0,0 +1,182 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient; + +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Body; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Http; +import com.iluwatar.dynamicproxy.tinyrestclient.annotation.Path; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.util.UriUtils; + +/** + * Class to handle all the http communication with a Rest API. + * It is supported by the HttpClient Java library. + */ +@Slf4j +public class TinyRestClient { + + private static Map httpAnnotationByMethod = new HashMap<>(); + + private String baseUrl; + private HttpClient httpClient; + + /** + * Class constructor. + * + * @param baseUrl Root url for endpoints. + * @param httpClient Handle the http communication. + */ + public TinyRestClient(String baseUrl, HttpClient httpClient) { + this.baseUrl = baseUrl; + this.httpClient = httpClient; + } + + /** + * Creates a http communication to request and receive data from an endpoint. + * + * @param method Interface's method which is annotated with a http method. + * @param args Method's arguments passed in the call. + * @return Response from the endpoint. + * @throws IOException Exception thrown when any fail happens in the call. + * @throws InterruptedException Exception thrown when call is interrupted. + */ + public Object send(Method method, Object[] args) throws IOException, InterruptedException { + var httpAnnotation = getHttpAnnotation(method); + if (httpAnnotation == null) { + return null; + } + var httpAnnotationName = httpAnnotation.annotationType().getSimpleName().toUpperCase(); + var url = baseUrl + buildUrl(method, args, httpAnnotation); + var bodyPublisher = buildBodyPublisher(method, args); + var httpRequest = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .method(httpAnnotationName, bodyPublisher) + .build(); + var httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + var statusCode = httpResponse.statusCode(); + if (statusCode >= HttpURLConnection.HTTP_BAD_REQUEST) { + var errorDetail = httpResponse.body(); + LOGGER.error("Error from server: " + errorDetail); + return null; + } + return getResponse(method, httpResponse); + } + + private String buildUrl(Method method, Object[] args, Annotation httpMethodAnnotation) { + var url = annotationValue(httpMethodAnnotation); + if (url == null) { + return ""; + } + var index = 0; + for (var parameter : method.getParameters()) { + var pathAnnotation = getAnnotationOf(parameter.getDeclaredAnnotations(), Path.class); + if (pathAnnotation != null) { + var pathParam = "{" + annotationValue(pathAnnotation) + "}"; + var pathValue = UriUtils.encodePath(args[index].toString(), StandardCharsets.UTF_8); + url = url.replace(pathParam, pathValue); + } + index++; + } + return url; + } + + private HttpRequest.BodyPublisher buildBodyPublisher(Method method, Object[] args) { + var index = 0; + for (var parameter : method.getParameters()) { + var bodyAnnotation = getAnnotationOf(parameter.getDeclaredAnnotations(), Body.class); + if (bodyAnnotation != null) { + var body = JsonUtil.objectToJson(args[index]); + return HttpRequest.BodyPublishers.ofString(body); + } + index++; + } + return HttpRequest.BodyPublishers.noBody(); + } + + private Object getResponse(Method method, HttpResponse httpResponse) { + var rawData = httpResponse.body(); + Type returnType = null; + try { + returnType = method.getGenericReturnType(); + } catch (Exception e) { + LOGGER.error("Cannot get the generic return type of the method " + method.getName() + "()"); + return null; + } + if (returnType instanceof ParameterizedType) { + Class responseClass = (Class) (((ParameterizedType) returnType) + .getActualTypeArguments()[0]); + return JsonUtil.jsonToList(rawData, responseClass); + } else { + Class responseClass = method.getReturnType(); + return JsonUtil.jsonToObject(rawData, responseClass); + } + } + + private Annotation getHttpAnnotation(Method method) { + return httpAnnotationByMethod.computeIfAbsent(method, m -> + Arrays.stream(m.getDeclaredAnnotations()) + .filter(annot -> annot.annotationType().isAnnotationPresent(Http.class)) + .findFirst().orElse(null)); + } + + private Annotation getAnnotationOf(Annotation[] annotations, Class clazz) { + return Arrays.stream(annotations) + .filter(annot -> annot.annotationType().equals(clazz)) + .findFirst().orElse(null); + } + + private String annotationValue(Annotation annotation) { + var valueMethod = Arrays.stream(annotation.annotationType().getDeclaredMethods()) + .filter(methodAnnot -> methodAnnot.getName().equals("value")) + .findFirst().orElse(null); + if (valueMethod == null) { + return null; + } + Object result; + try { + result = valueMethod.invoke(annotation, (Object[]) null); + } catch (Exception e) { + LOGGER.error("Cannot read the value " + annotation.annotationType().getSimpleName() + + "." + valueMethod.getName() + "()", e); + result = null; + } + return (result instanceof String strResult ? strResult : null); + } +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Body.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Body.java new file mode 100644 index 000000000..1d91c0af1 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Body.java @@ -0,0 +1,39 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a method's parameter as a Body parameter. + * It is typically used on Post and Put http methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Body { +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Delete.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Delete.java new file mode 100644 index 000000000..5f21457ab --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Delete.java @@ -0,0 +1,45 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark an interface's method as a DELETE http method. + */ +@Http +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Delete { + /** + * Set the url for this http method. + * + * @return Url address. + */ + String value() default ""; +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Get.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Get.java new file mode 100644 index 000000000..163a65ad9 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Get.java @@ -0,0 +1,45 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark an interface's method as a GET http method. + */ +@Http +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Get { + /** + * Set the url for this http method. + * + * @return Url address. + */ + String value() default ""; +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Http.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Http.java new file mode 100644 index 000000000..beabcdb69 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Http.java @@ -0,0 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark other annotations to be recognized as http methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Http { +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Path.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Path.java new file mode 100644 index 000000000..0f2bcd007 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Path.java @@ -0,0 +1,44 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a method's parameter as a Path parameter. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Path { + /** + * Path parameter to be replaced in the url. + * + * @return Path parameter. + */ + String value(); +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Post.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Post.java new file mode 100644 index 000000000..9885389c4 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Post.java @@ -0,0 +1,45 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark an interface's method as a POST http method. + */ +@Http +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Post { + /** + * Set the url for this http method. + * + * @return Url address. + */ + String value() default ""; +} diff --git a/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Put.java b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Put.java new file mode 100644 index 000000000..851303ed8 --- /dev/null +++ b/dynamic-proxy/src/main/java/com/iluwatar/dynamicproxy/tinyrestclient/annotation/Put.java @@ -0,0 +1,45 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy.tinyrestclient.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark an interface's method as a PUT http method. + */ +@Http +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Put { + /** + * Set the url for this http method. + * + * @return Url address. + */ + String value() default ""; +} diff --git a/dynamic-proxy/src/test/java/com/iluwatar/dynamicproxy/AppTest.java b/dynamic-proxy/src/test/java/com/iluwatar/dynamicproxy/AppTest.java new file mode 100644 index 000000000..72412ca5b --- /dev/null +++ b/dynamic-proxy/src/test/java/com/iluwatar/dynamicproxy/AppTest.java @@ -0,0 +1,37 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.dynamicproxy; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class AppTest { + + @Test + void shouldRunAppWithoutExceptions() { + assertDoesNotThrow(() -> App.main(null)); + } +} diff --git a/pom.xml b/pom.xml index 32cbbaa15..a9273da8e 100644 --- a/pom.xml +++ b/pom.xml @@ -213,6 +213,7 @@ health-check notification single-table-inheritance + dynamic-proxy gateway