Skip to content

Commit

Permalink
Merge pull request #34 from sashirestela/release_0_12
Browse files Browse the repository at this point in the history
Fixing code, tests and adding javadoc comments
  • Loading branch information
sashirestela authored Dec 29, 2023
2 parents b8a44de + 455268d commit b562c01
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.http.HttpProcessor;
import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.support.CleverClientSSE;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Singular;

/**
* Main class and entry point to use this library. This is a kind of wrapper
* that makes it easier to use the Java's HttpClient component to call http
* services by using annotated interfaces.
*/
@Getter
public class CleverClient {
private static Logger logger = LoggerFactory.getLogger(CleverClient.class);
Expand All @@ -23,17 +29,40 @@ public class CleverClient {
private HttpClient httpClient;
private HttpProcessor httpProcessor;

/**
* Constructor to create an instance of CleverClient.
*
* @param urlBase Root of the url of the API service to call. Mandatory.
* @param headers Http headers for all the API service. Header's name and
* value must be individual entries in the list. Optional.
* @param httpClient Custom Java's HttpClient component. One is created by
* default if none is passed. Optional.
* @param endOfStream Text used to mark the final of streams when handling
* server sent events (SSE). Optional.
*/
@Builder
public CleverClient(@NonNull String urlBase, @Singular List<String> headers, HttpClient httpClient,
String endOfStream) {
this.urlBase = urlBase;
this.headers = Optional.ofNullable(headers).orElse(List.of());
if (this.headers.size() % 2 > 0) {
throw new CleverClientException("Headers must be entered as pair of values in the list.", null, null);
}
this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient());
CleverClientSSE.setEndOfStream(endOfStream);
this.httpProcessor = new HttpProcessor(this.httpClient, this.urlBase, this.headers);
this.httpProcessor = new HttpProcessor(this.urlBase, this.headers, this.httpClient);
logger.debug("CleverClient has been created.");
}

/**
* Creates an instance of an annotated interface that represents a resource of
* the API service and its methods represent the endpoints that we can call:
* Get, Post, Put, Patch, Delete.
*
* @param <T> Type of the interface.
* @param interfaceClass The interface to be instanced.
* @return A proxy instance of the interface.
*/
public <T> T create(Class<T> interfaceClass) {
return this.httpProcessor.createProxy(interfaceClass);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.sender.HttpSenderFactory;
import io.github.sashirestela.cleverclient.support.HttpMultipart;
import io.github.sashirestela.cleverclient.support.ReturnType;
import io.github.sashirestela.cleverclient.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;

/**
* HttpConnector prepares the request and receives the response to/from the
* Java's HttpClient component.
*/
@AllArgsConstructor
@Builder
public class HttpConnector {
Expand All @@ -27,9 +33,16 @@ public class HttpConnector {
private boolean isMultipart;
private String[] headersArray;

/**
* Prepares the request to call Java's HttpClient and delegates it to a
* specialized HttpSender based on the method's return type.
*
* @return The response coming from the HttpSender's sendRequest method.
*/
public Object sendRequest() {
var bodyPublisher = createBodyPublisher(bodyObject, isMultipart);
var responseClass = returnType.getBaseClass();
var genericClass = returnType.getGenericClassIfExists();
HttpRequest httpRequest = null;
if (headersArray.length > 0) {
httpRequest = HttpRequest.newBuilder()
Expand All @@ -44,11 +57,7 @@ public Object sendRequest() {
.build();
}
var httpSender = HttpSenderFactory.get().createSender(returnType);
if (returnType.isGeneric()) {
return httpSender.sendRequest(httpClient, httpRequest, responseClass, returnType.getGenericClass());
} else {
return httpSender.sendRequest(httpClient, httpRequest, responseClass, null);
}
return httpSender.sendRequest(httpClient, httpRequest, responseClass, genericClass);
}

private BodyPublisher createBodyPublisher(Object bodyObject, boolean isMultipart) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.net.http.HttpClient;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -17,26 +16,38 @@
import io.github.sashirestela.cleverclient.util.Constant;
import io.github.sashirestela.cleverclient.util.ReflectUtil;

/**
* HttpProcessor orchestrates all the http interaction.
*/
public class HttpProcessor implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(HttpProcessor.class);

private HttpClient httpClient;
private String urlBase;
private List<String> headers;

public HttpProcessor(HttpClient httpClient, String urlBase, List<String> headers) {
this.httpClient = httpClient;
/**
* Constructor to create an instance of HttpProcessor.
*
* @param httpClient Java's HttpClient component that solves the http calling.
* @param urlBase Root of the url of the API service to call.
* @param headers Http headers for all the API service.
*/
public HttpProcessor(String urlBase, List<String> headers, HttpClient httpClient) {
this.urlBase = urlBase;
this.headers = Optional.ofNullable(headers).orElse(List.of());
this.headers = headers;
this.httpClient = httpClient;
}

/**
* Creates a generic dynamic proxy with a new {@link HttpInvocationHandler
* HttpInvocationHandler} object which will resolve the requests.
* Creates a generic dynamic proxy with this HttpProcessor object acting as an
* InvocationHandler to resolve the requests arriving to the proxy. Previously,
* the interface metadata is collected and stored in memory to be used later and
* avoid to use Reflection calls.
*
* @param <T> A generic interface.
* @param interfaceClass Service of a generic interface
* @return A "virtual" instance for the interface.
* @param <T> Type of the interface.
* @param interfaceClass The interface to be instanced.
* @return A proxy instance of the interface.
*/
public <T> T createProxy(Class<T> interfaceClass) {
InterfaceMetadataStore.one().save(interfaceClass);
Expand All @@ -45,6 +56,19 @@ public <T> T createProxy(Class<T> interfaceClass) {
return proxy;
}

/**
* Method automatically called whenever an interface's method is called. It
* handles default methods directly. Non-default methods are solved by calling
* HttpConnector.
*
* @param proxy The proxy instance that the method was invoked on.
* @param method The Method instance corresponding to the interface method
* invoked on the proxy instance.
* @param arguments An array of objects containing the values of the arguments
* passed in the method invocation on the proxy instance, or
* null if interface method takes no arguments.
* @return The value to return from the method invocation on the proxy instance.
*/
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
logger.debug("Invoked Method : {}.{}()", method.getDeclaringClass().getSimpleName(), method.getName());
Expand All @@ -66,6 +90,18 @@ public Object invoke(Object proxy, Method method, Object[] arguments) throws Thr
}
}

/**
* Reads the interface method metadata from memory and uses them to prepare an
* HttpConnector object that will resend the request to the Java's HttpClient
* and will receive the response. This method is called from the invoke mehod.
*
* @param method The Method instance corresponding to the interface method
* invoked on the proxy instance.
* @param arguments An array of objects containing the values of the arguments
* passed in the method invocation on the proxy instance, or
* null if interface method takes no arguments.
* @return The response coming from the HttpConnector's sendRequest method.
*/
private Object resolve(Method method, Object[] arguments) {
var interfaceMetadata = InterfaceMetadataStore.one().get(method.getDeclaringClass());
var methodMetadata = interfaceMetadata.getMethodBySignature().get(method.toString());
Expand Down Expand Up @@ -112,7 +148,7 @@ private List<String> calculateHeaderContentType(Object bodyObject, boolean isMul

private String printHeaders(List<String> headers) {
var print = "{";
for(var i = 0; i < headers.size(); i++) {
for (var i = 0; i < headers.size(); i++) {
if (i > 1) {
print += ", ";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.Map;
import java.util.stream.Collectors;

import io.github.sashirestela.cleverclient.http.ReturnType;
import io.github.sashirestela.cleverclient.support.ReturnType;
import lombok.Builder;
import lombok.Value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.annotation.HttpMethod;
import io.github.sashirestela.cleverclient.http.ReturnType;
import io.github.sashirestela.cleverclient.metadata.InterfaceMetadata.AnnotationMetadata;
import io.github.sashirestela.cleverclient.metadata.InterfaceMetadata.MethodMetadata;
import io.github.sashirestela.cleverclient.metadata.InterfaceMetadata.ParameterMetadata;
import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.support.ReturnType;
import io.github.sashirestela.cleverclient.util.CommonUtil;
import io.github.sashirestela.cleverclient.util.Constant;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,35 @@
import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.util.CommonUtil;

/**
* HttpSender is an abstract class for a set of concrete classes that implement
* different interactions with the Java's HttpClient based on the method's
* return type.
*/
public abstract class HttpSender {
protected static Logger logger = LoggerFactory.getLogger(HttpSender.class);

/**
* Method to be implementd for concrete classes to send request to the Java's
* HttpClient and receive response.
*
* @param <S> Type of a generic class if exists.
* @param <T> 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.
* @return Response coming from Java's HttpClient.
*/
public abstract <S, T> Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Class<T> responseClass,
Class<S> genericClass);

/**
* Exception handling that will be called by any concrete class.
*
* @param response Java's HttpResponse component.
* @param clazz Response class.
*/
@SuppressWarnings("unchecked")
protected void throwExceptionIfErrorIsPresent(HttpResponse<?> response, Class<?> clazz) {
logger.debug("Response Code : {}", response.statusCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.http.ReturnType;
import io.github.sashirestela.cleverclient.http.ReturnType.Category;
import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.support.ReturnType;
import io.github.sashirestela.cleverclient.support.ReturnType.Category;

/**
* Factory for the abstrac class HttpSender.
*/
public class HttpSenderFactory {
private static Logger logger = LoggerFactory.getLogger(HttpSenderFactory.class);

Expand Down Expand Up @@ -40,6 +43,12 @@ public static HttpSenderFactory get() {
return factory;
}

/**
* Instances a HttpSender concrete class based on the return type.
*
* @param returnType The method return type.
* @return A HttpSender concrete class.
*/
public HttpSender createSender(ReturnType returnType) {
HttpSender sender = null;
var category = returnType.category();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.sashirestela.cleverclient.http;
package io.github.sashirestela.cleverclient.support;

import java.io.IOException;
import java.net.URL;
Expand All @@ -9,7 +9,6 @@
import java.util.List;
import java.util.Map;

import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.util.Constant;

public class HttpMultipart {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.sashirestela.cleverclient.http;
package io.github.sashirestela.cleverclient.support;

import java.lang.reflect.Method;

Expand Down Expand Up @@ -44,8 +44,8 @@ public Class<?> getBaseClass() {
return getClass(lastIndex);
}

public Class<?> getGenericClass() {
return getClass(prevLastIndex);
public Class<?> getGenericClassIfExists() {
return isGeneric() ? getClass(prevLastIndex) : null;
}

private Class<?> getClass(int index) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.http.HttpClient;
import java.util.List;
Expand All @@ -10,12 +12,13 @@
import org.junit.jupiter.api.Test;

import io.github.sashirestela.cleverclient.annotation.GET;
import io.github.sashirestela.cleverclient.support.CleverClientException;

class CleverClientTest {

@Test
void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties() {
CleverClient cleverClient = CleverClient.builder()
var cleverClient = CleverClient.builder()
.urlBase("https://test")
.build();
assertEquals(List.of(), cleverClient.getHeaders());
Expand All @@ -26,13 +29,38 @@ void shouldSetPropertiesToDefaultValuesWhenBuilderIsCalledWithoutThoseProperties

@Test
void shouldImplementInterfaceWhenCallingCreate() {
CleverClient cleverClient = CleverClient.builder()
var cleverClient = CleverClient.builder()
.urlBase("https://test")
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]")
.build();
TestCleverClient test = cleverClient.create(TestCleverClient.class);
var test = cleverClient.create(TestCleverClient.class);
assertNotNull(test);
}

@Test
void shouldThrownExceptionWhenTryingToPassAnEmptyUrlBase() {
var cleverClientBuilder = CleverClient.builder()
.header("headerName")
.header("headerValue")
.httpClient(HttpClient.newHttpClient())
.endOfStream("[DONE]");
assertThrows(NullPointerException.class,
() -> cleverClientBuilder.build());
}

@Test
void shouldThrownExceptionWhenTryingToPassAnOddNumbersOfHeaders() {
var cleverClientBuilder = CleverClient.builder()
.urlBase("http://test")
.header("oneHeader");
Exception exception = assertThrows(CleverClientException.class,
() -> cleverClientBuilder.build());
assertTrue(exception.getMessage().equals("Headers must be entered as pair of values in the list."));
}

interface TestCleverClient {
@GET("/api/")
CompletableFuture<String> getText();
Expand Down
Loading

0 comments on commit b562c01

Please sign in to comment.