Skip to content

Commit

Permalink
Merge pull request #49 from sashirestela/46-extend-interceptor-to-han…
Browse files Browse the repository at this point in the history
…dle-body-request

Extend interceptor to handle body request
  • Loading branch information
sashirestela authored Feb 12, 2024
2 parents be20041 + 000b297 commit 0251edb
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 82 deletions.
27 changes: 15 additions & 12 deletions src/main/java/io/github/sashirestela/cleverclient/CleverClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import java.net.http.HttpClient;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.http.HttpProcessor;
import io.github.sashirestela.cleverclient.http.HttpRequestData;
import io.github.sashirestela.cleverclient.support.CleverClientSSE;
import io.github.sashirestela.cleverclient.util.CommonUtil;
import lombok.Builder;
Expand All @@ -29,33 +30,35 @@ public class CleverClient {
private final String baseUrl;
private final Map<String, String> headers;
private final HttpClient httpClient;
private final Function<String, String> urlInterceptor;
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final HttpProcessor httpProcessor;

/**
* Constructor to create an instance of CleverClient.
*
* @param baseUrl Root of the url of the API service to call. Mandatory.
* @param headers Http headers for all the API service. Optional.
* @param httpClient Custom Java's HttpClient component. One is created by
* default if none is passed. Optional.
* @param urlInterceptor Function to modify the url once it has been built.
* @param endOfStream Text used to mark the final of streams when handling
* server sent events (SSE). Optional.
* @param baseUrl Root of the url of the API service to call.
* Mandatory.
* @param headers Http headers for all the API service. Optional.
* @param httpClient Custom Java's HttpClient component. One is created
* by default if none is passed. Optional.
* @param requestInterceptor Function to modify the request once it has been
* built.
* @param endOfStream Text used to mark the final of streams when
* handling server sent events (SSE). Optional.
*/
@Builder
public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> headers, HttpClient httpClient,
Function<String, String> urlInterceptor, String endOfStream) {
UnaryOperator<HttpRequestData> requestInterceptor, String endOfStream) {
this.baseUrl = baseUrl;
this.headers = Optional.ofNullable(headers).orElse(Map.of());
this.httpClient = Optional.ofNullable(httpClient).orElse(HttpClient.newHttpClient());
this.urlInterceptor = urlInterceptor;
this.requestInterceptor = requestInterceptor;
CleverClientSSE.setEndOfStream(endOfStream);
this.httpProcessor = HttpProcessor.builder()
.baseUrl(this.baseUrl)
.headers(CommonUtil.mapToListOfString(this.headers))
.httpClient(this.httpClient)
.urlInterceptor(this.urlInterceptor)
.requestInterceptor(this.requestInterceptor)
.build();
logger.debug("CleverClient has been created.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

Expand All @@ -23,15 +27,16 @@
@AllArgsConstructor
@Builder
public class HttpConnector {
private static Logger logger = LoggerFactory.getLogger(HttpConnector.class);
private static final Logger logger = LoggerFactory.getLogger(HttpConnector.class);

private HttpClient httpClient;
private String url;
private String httpMethod;
private ReturnType returnType;
private Object bodyObject;
private boolean isMultipart;
private String[] headersArray;
private ContentType contentType;
private List<String> headers;
private UnaryOperator<HttpRequestData> requestInterceptor;

/**
* Prepares the request to call Java's HttpClient and delegates it to a
Expand All @@ -40,9 +45,16 @@ public class HttpConnector {
* @return The response coming from the HttpSender's sendRequest method.
*/
public Object sendRequest() {
var bodyPublisher = createBodyPublisher(bodyObject, isMultipart);
if (requestInterceptor != null) {
interceptRequest();
}
logger.debug("Http Call : {} {}", httpMethod, url);
logger.debug("Request Headers : {}", printHeaders(headers));

var bodyPublisher = createBodyPublisher(bodyObject, contentType);
var responseClass = returnType.getBaseClass();
var genericClass = returnType.getGenericClassIfExists();
var headersArray = headers.toArray(new String[0]);
HttpRequest httpRequest = null;
if (headersArray.length > 0) {
httpRequest = HttpRequest.newBuilder()
Expand All @@ -60,21 +72,56 @@ public Object sendRequest() {
return httpSender.sendRequest(httpClient, httpRequest, responseClass, genericClass);
}

private BodyPublisher createBodyPublisher(Object bodyObject, boolean isMultipart) {
private void interceptRequest() {
var httpRequestData = HttpRequestData.builder()
.url(url)
.body(bodyObject)
.headers(CommonUtil.listToMapOfString(headers))
.httpMethod(httpMethod)
.contentType(contentType)
.build();

httpRequestData = requestInterceptor.apply(httpRequestData);

url = httpRequestData.getUrl();
bodyObject = httpRequestData.getBody();
headers = CommonUtil.mapToListOfString(httpRequestData.getHeaders());
}

@SuppressWarnings("unchecked")
private BodyPublisher createBodyPublisher(Object bodyObject, ContentType contentType) {
BodyPublisher bodyPublisher = null;
if (bodyObject == null) {
if (contentType == null) {
logger.debug("Request Body : (Empty)");
bodyPublisher = BodyPublishers.noBody();
} else if (isMultipart) {
var data = JsonUtil.objectToMap(bodyObject);
var requestBytes = HttpMultipart.toByteArrays(data);
logger.debug("Request Body : {}", data);
bodyPublisher = BodyPublishers.ofByteArrays(requestBytes);
} else {
var requestString = JsonUtil.objectToJson(bodyObject);
logger.debug("Request Body : {}", requestString);
bodyPublisher = BodyPublishers.ofString(requestString);
switch (contentType) {
case MULTIPART_FORMDATA:
logger.debug("Request Body : {}", (Map<String, Object>) bodyObject);
var bodyBytes = HttpMultipart.toByteArrays((Map<String, Object>) bodyObject);
bodyPublisher = BodyPublishers.ofByteArrays(bodyBytes);
break;

case APPLICATION_JSON:
logger.debug("Request Body : {}", (String) bodyObject);
bodyPublisher = BodyPublishers.ofString((String) bodyObject);
break;
}
}
return bodyPublisher;
}

private String printHeaders(List<String> headers) {
var print = "{";
for (var i = 0; i < headers.size(); i++) {
if (i > 1) {
print += ", ";
}
var headerKey = headers.get(i++);
var headerVal = headerKey.equals("Authorization") ? "*".repeat(10) : headers.get(i);
print += headerKey + " = " + headerVal;
}
print += "}";
return print;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import java.net.http.HttpClient;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.sashirestela.cleverclient.metadata.InterfaceMetadata.MethodMetadata;
import io.github.sashirestela.cleverclient.support.ContentType;
import io.github.sashirestela.cleverclient.metadata.InterfaceMetadataStore;
import io.github.sashirestela.cleverclient.util.Constant;
import io.github.sashirestela.cleverclient.util.JsonUtil;
import io.github.sashirestela.cleverclient.util.ReflectUtil;
import lombok.Builder;

Expand All @@ -28,7 +29,7 @@ public class HttpProcessor implements InvocationHandler {
private final String baseUrl;
private final List<String> headers;
private final HttpClient httpClient;
private final Function<String, String> urlInterceptor;
private final UnaryOperator<HttpRequestData> requestInterceptor;

/**
* Creates a generic dynamic proxy with this HttpProcessor object acting as an
Expand Down Expand Up @@ -98,59 +99,49 @@ private Object resolve(Method method, Object[] arguments) {
var methodMetadata = interfaceMetadata.getMethodBySignature().get(method.toString());
var urlMethod = interfaceMetadata.getFullUrlByMethod(methodMetadata);
var url = baseUrl + URLBuilder.one().build(urlMethod, methodMetadata, arguments);
if (urlInterceptor != null) {
url = urlInterceptor.apply(url);
}
var httpMethod = methodMetadata.getHttpAnnotationName();
var returnType = methodMetadata.getReturnType();
var isMultipart = methodMetadata.isMultipart();
var bodyObject = calculateBodyObject(methodMetadata, arguments);
var contentType = methodMetadata.getContentType();
var fullHeaders = new ArrayList<>(this.headers);
fullHeaders.addAll(calculateHeaderContentType(bodyObject, isMultipart));
fullHeaders.addAll(calculateHeaderContentType(contentType));
fullHeaders.addAll(interfaceMetadata.getFullHeadersByMethod(methodMetadata));
var fullHeadersArray = fullHeaders.toArray(new String[0]);
var httpConnector = HttpConnector.builder()
.httpClient(httpClient)
.url(url)
.httpMethod(httpMethod)
.returnType(returnType)
.bodyObject(bodyObject)
.isMultipart(isMultipart)
.headersArray(fullHeadersArray)
.contentType(contentType)
.headers(fullHeaders)
.requestInterceptor(requestInterceptor)
.build();
logger.debug("Http Call : {} {}", httpMethod, url);
logger.debug("Request Headers : {}", printHeaders(fullHeaders));
return httpConnector.sendRequest();
}

private Object calculateBodyObject(MethodMetadata methodMetadata, Object[] arguments) {
var indexBody = methodMetadata.getBodyIndex();
return indexBody >= 0 ? arguments[indexBody] : null;
}

private List<String> calculateHeaderContentType(Object bodyObject, boolean isMultipart) {
List<String> headerContentType = new ArrayList<>();
var bodyIndex = methodMetadata.getBodyIndex();
var bodyObject = bodyIndex >= 0 ? arguments[bodyIndex] : null;
if (bodyObject != null) {
headerContentType.add(Constant.HEADER_CONTENT_TYPE);
var contentType = isMultipart
? Constant.TYPE_MULTIPART + Constant.BOUNDARY_TITLE + "\"" + Constant.BOUNDARY_VALUE + "\""
: Constant.TYPE_APP_JSON;
headerContentType.add(contentType);
switch (methodMetadata.getContentType()) {
case MULTIPART_FORMDATA:
bodyObject = JsonUtil.objectToMap(bodyObject);
break;
case APPLICATION_JSON:
bodyObject = JsonUtil.objectToJson(bodyObject);
break;
}
}
return headerContentType;
return bodyObject;
}

private String printHeaders(List<String> headers) {
var print = "{";
for (var i = 0; i < headers.size(); i++) {
if (i > 1) {
print += ", ";
}
var headerKey = headers.get(i++);
var headerVal = headerKey.equals("Authorization") ? "*".repeat(10) : headers.get(i);
print += headerKey + " = " + headerVal;
private List<String> calculateHeaderContentType(ContentType contentType) {
final String HEADER_CONTENT_TYPE = "Content-Type";
List<String> headerContentType = new ArrayList<>();
if (contentType != null) {
headerContentType.add(HEADER_CONTENT_TYPE);
headerContentType.add(contentType.getMimeType() + contentType.getDetails());
}
print += "}";
return print;
return headerContentType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.sashirestela.cleverclient.http;

import java.util.Map;

import io.github.sashirestela.cleverclient.support.ContentType;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Builder
@Getter
public class HttpRequestData {
@Setter private String url;
@Setter private Object body;
@Setter private Map<String, String> headers;
private String httpMethod;
private ContentType contentType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;
import java.util.stream.Collectors;

import io.github.sashirestela.cleverclient.support.ContentType;
import io.github.sashirestela.cleverclient.support.ReturnType;
import lombok.Builder;
import lombok.Value;
Expand Down Expand Up @@ -75,7 +76,15 @@ public String getHttpAnnotationName() {
.getName();
}

public boolean isMultipart() {
public ContentType getContentType() {
return getBodyIndex() == -1
? null
: isMultipart()
? ContentType.MULTIPART_FORMDATA
: ContentType.APPLICATION_JSON;
}

private boolean isMultipart() {
return annotations.stream()
.anyMatch(annot -> annot.getName().equals(ANNOT_MULTIPART));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.github.sashirestela.cleverclient.support;

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

public enum ContentType {
MULTIPART_FORMDATA(
"multipart/form-data",
"; boundary=\"" + Constant.BOUNDARY_VALUE + "\""),
APPLICATION_JSON(
"application/json",
"");

private String mimeType;
private String details;

ContentType(String mimeType, String details) {
this.mimeType = mimeType;
this.details = details;
}

public String getMimeType() {
return this.mimeType;
}

public String getDetails() {
return this.details;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,9 @@ public static List<String> mapToListOfString(Map<String, String> map) {
}).collect(Collectors.counting());
return list;
}

public static Map<String, String> listToMapOfString(List<String> list) {
var array = list.toArray(new String[0]);
return createMapString(array);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ public class Constant {

private Constant() {
}

public static final String JSON_EMPTY_CLASS = "{\"type\":\"object\",\"properties\":{}}";

public static final String HEADER_CONTENT_TYPE = "Content-Type";

public static final String TYPE_APP_JSON = "application/json";
public static final String TYPE_MULTIPART = "multipart/form-data";

public static final String BOUNDARY_TITLE = "; boundary=";
public static final String BOUNDARY_VALUE = new BigInteger(256, new Random()).toString();

public static final String REGEX_PATH_PARAM_URL = "\\{(.*?)\\}";
Expand Down
Loading

0 comments on commit 0251edb

Please sign in to comment.