From 662d24d3401b670aab8117bb1864d60cc6b3ed2f Mon Sep 17 00:00:00 2001 From: sbansla <104902068+sbansla@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:51:22 +0530 Subject: [PATCH] chore: created exceptions, restclient, httpclient, request and response (#751) * created exceptions, restclient, httpclient, request and response --- pom.xml | 29 ++++ .../java/com/sendgrid/ApiKeySendGrid.java | 91 ++++++++++ .../com/sendgrid/base/apikey/ApiKeyBase.java | 17 ++ .../java/com/sendgrid/constant/Config.java | 8 + .../com/sendgrid/constant/EnumConstants.java | 15 ++ .../com/sendgrid/constant/ErrorMessages.java | 10 ++ .../java/com/sendgrid/converter/Promoter.java | 61 +++++++ .../exception/ApiConnectionException.java | 15 ++ .../sendgrid/exception/ApiErrorResponse.java | 29 ++++ .../com/sendgrid/exception/ApiException.java | 89 ++++++++++ .../exception/AuthenticationException.java | 10 ++ .../exception/InvalidRequestException.java | 19 +++ .../com/sendgrid/exception/RestException.java | 82 +++++++++ .../sendgrid/exception/SendgridException.java | 14 ++ .../com/sendgrid/http/ApiKeyRestClient.java | 98 +++++++++++ .../java/com/sendgrid/http/ApiResponse.java | 42 +++++ .../java/com/sendgrid/http/HttpMethod.java | 29 ++++ src/main/java/com/sendgrid/http/Request.java | 92 ++++++++++ src/main/java/com/sendgrid/http/Response.java | 120 +++++++++++++ .../com/sendgrid/http/auth/AuthStrategy.java | 7 + .../sendgrid/http/auth/BasicAuthStrategy.java | 18 ++ .../com/sendgrid/http/auth/TokenStrategy.java | 13 ++ .../http/httpclient/ApiKeyHttpClient.java | 158 ++++++++++++++++++ .../sendgrid/http/httpclient/HttpClient.java | 138 +++++++++++++++ .../sendgrid/http/httpclient/HttpUtility.java | 36 ++++ src/main/java/com/sendgrid/util/JsonUtil.java | 92 ++++++++++ src/main/java/com/sendgrid/util/Matcher.java | 21 +++ src/main/java/com/sendgrid/util/Utility.java | 38 +++++ 28 files changed, 1391 insertions(+) create mode 100644 src/main/java/com/sendgrid/ApiKeySendGrid.java create mode 100644 src/main/java/com/sendgrid/base/apikey/ApiKeyBase.java create mode 100644 src/main/java/com/sendgrid/constant/Config.java create mode 100644 src/main/java/com/sendgrid/constant/EnumConstants.java create mode 100644 src/main/java/com/sendgrid/constant/ErrorMessages.java create mode 100644 src/main/java/com/sendgrid/converter/Promoter.java create mode 100644 src/main/java/com/sendgrid/exception/ApiConnectionException.java create mode 100644 src/main/java/com/sendgrid/exception/ApiErrorResponse.java create mode 100644 src/main/java/com/sendgrid/exception/ApiException.java create mode 100644 src/main/java/com/sendgrid/exception/AuthenticationException.java create mode 100644 src/main/java/com/sendgrid/exception/InvalidRequestException.java create mode 100644 src/main/java/com/sendgrid/exception/RestException.java create mode 100644 src/main/java/com/sendgrid/exception/SendgridException.java create mode 100644 src/main/java/com/sendgrid/http/ApiKeyRestClient.java create mode 100644 src/main/java/com/sendgrid/http/ApiResponse.java create mode 100644 src/main/java/com/sendgrid/http/HttpMethod.java create mode 100644 src/main/java/com/sendgrid/http/Request.java create mode 100644 src/main/java/com/sendgrid/http/Response.java create mode 100644 src/main/java/com/sendgrid/http/auth/AuthStrategy.java create mode 100644 src/main/java/com/sendgrid/http/auth/BasicAuthStrategy.java create mode 100644 src/main/java/com/sendgrid/http/auth/TokenStrategy.java create mode 100644 src/main/java/com/sendgrid/http/httpclient/ApiKeyHttpClient.java create mode 100644 src/main/java/com/sendgrid/http/httpclient/HttpClient.java create mode 100644 src/main/java/com/sendgrid/http/httpclient/HttpUtility.java create mode 100644 src/main/java/com/sendgrid/util/JsonUtil.java create mode 100644 src/main/java/com/sendgrid/util/Matcher.java create mode 100644 src/main/java/com/sendgrid/util/Utility.java diff --git a/pom.xml b/pom.xml index 541f1964..3ac5876a 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,17 @@ maven-scm-provider-gitexe 1.8.1 + + org.projectlombok + lombok + 1.18.30 + compile + + + org.slf4j + slf4j-api + 1.7.30 + @@ -245,10 +256,12 @@ spotbugs 4.0.4 + + thinkingserious @@ -264,6 +277,11 @@ java-http-client 4.5.0 + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + com.fasterxml.jackson.core jackson-core @@ -296,6 +314,17 @@ bcprov-jdk18on 1.76 + + org.projectlombok + lombok + 1.18.30 + compile + + + org.slf4j + slf4j-api + 1.7.30 + diff --git a/src/main/java/com/sendgrid/ApiKeySendGrid.java b/src/main/java/com/sendgrid/ApiKeySendGrid.java new file mode 100644 index 00000000..76b6f39b --- /dev/null +++ b/src/main/java/com/sendgrid/ApiKeySendGrid.java @@ -0,0 +1,91 @@ +package com.sendgrid; + +import com.sendgrid.constant.ErrorMessages; +import com.sendgrid.exception.AuthenticationException; +import com.sendgrid.http.ApiKeyRestClient; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class ApiKeySendGrid { + private static String apiKey = System.getenv("SENDGRID_API_KEY"); + private static String region = System.getenv("SENDGRID_REGION"); + @Getter + private static List userAgentExtensions; + private static volatile ApiKeyRestClient apiKeyRestClient; + + private ApiKeySendGrid() { + } + + public static synchronized void init(final String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + throw new AuthenticationException(String.format(ErrorMessages.EMPTY_STRING, "API_KEY")); + } + ApiKeySendGrid.apiKey = apiKey; + } + + + // Explore Data Residency + /** + * Set the region. + * + * @param region region to make request + * Global Region api.sendgrid.com + * EU Region api.eu.sendgrid.com + */ + public static synchronized void setRegion(final String region) { + if (region == null || region.isEmpty()) { + throw new AuthenticationException(String.format(ErrorMessages.EMPTY_STRING, "REGION")); + } + if (!Objects.equals(region, ApiKeySendGrid.region)) { + ApiKeySendGrid.invalidate(); + } + ApiKeySendGrid.region = region; + } + + public static synchronized void setUserAgentExtensions(final List userAgentExtensions) { + if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) { + ApiKeySendGrid.userAgentExtensions = new ArrayList<>(userAgentExtensions); + } else { + // In case a developer wants to reset userAgentExtensions + ApiKeySendGrid.userAgentExtensions = null; + } + } + + + public static ApiKeyRestClient getRestClient() { + if (ApiKeySendGrid.apiKeyRestClient == null) { + synchronized (ApiKeySendGrid.class) { + if (ApiKeySendGrid.apiKeyRestClient == null) { + ApiKeySendGrid.apiKeyRestClient = buildRestClient(); + } + } + } + + return ApiKeySendGrid.apiKeyRestClient; + } + + private static ApiKeyRestClient buildRestClient() { + if (ApiKeySendGrid.apiKey == null) { + throw new AuthenticationException( + "Api Key is not initialized, please call ApiKeySendGrid.init()" + ); + } + ApiKeyRestClient.Builder builder = new ApiKeyRestClient.Builder(ApiKeySendGrid.apiKey); + if (userAgentExtensions != null) { + builder.userAgentExtensions(ApiKeySendGrid.userAgentExtensions); + } + // TODO: Check if it mandatory to fetch region from customer. + builder.region(region); + return builder.build(); + } + + /** + * Invalidates the volatile state held in the Sendgrid singleton. + */ + private static void invalidate() { + ApiKeySendGrid.apiKeyRestClient = null; + } +} diff --git a/src/main/java/com/sendgrid/base/apikey/ApiKeyBase.java b/src/main/java/com/sendgrid/base/apikey/ApiKeyBase.java new file mode 100644 index 00000000..ebd2720a --- /dev/null +++ b/src/main/java/com/sendgrid/base/apikey/ApiKeyBase.java @@ -0,0 +1,17 @@ +package com.sendgrid.base.apikey; + +import com.sendgrid.ApiKeySendGrid; +import com.sendgrid.constant.ErrorMessages; +import com.sendgrid.http.ApiResponse; +import com.sendgrid.http.ApiKeyRestClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ApiKeyBase { + private static final Logger logger = LoggerFactory.getLogger(ApiKeyBase.class); + public ApiResponse send() { + logger.debug(String.format(ErrorMessages.DEFAULT_REST_CLIENT, "ApiKeyBase")); + return send(ApiKeySendGrid.getRestClient()); + } + public abstract ApiResponse send(final ApiKeyRestClient client); +} diff --git a/src/main/java/com/sendgrid/constant/Config.java b/src/main/java/com/sendgrid/constant/Config.java new file mode 100644 index 00000000..7854b460 --- /dev/null +++ b/src/main/java/com/sendgrid/constant/Config.java @@ -0,0 +1,8 @@ +package com.sendgrid.constant; + +public class Config { + public static final String VERSION = "5.0.0-rc.0"; + public static final String JAVA_VERSION = System.getProperty("java.version"); + public static final String OS_NAME = System.getProperty("os.name"); + public static final String OS_ARCH = System.getProperty("os.arch"); +} diff --git a/src/main/java/com/sendgrid/constant/EnumConstants.java b/src/main/java/com/sendgrid/constant/EnumConstants.java new file mode 100644 index 00000000..54c80698 --- /dev/null +++ b/src/main/java/com/sendgrid/constant/EnumConstants.java @@ -0,0 +1,15 @@ +package com.sendgrid.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +public class EnumConstants { + @Getter + @RequiredArgsConstructor + public enum ContentType { + JSON("application/json"), + FORM_URLENCODED("application/x-www-form-urlencoded"); + + private final String value; + } +} diff --git a/src/main/java/com/sendgrid/constant/ErrorMessages.java b/src/main/java/com/sendgrid/constant/ErrorMessages.java new file mode 100644 index 00000000..2000a750 --- /dev/null +++ b/src/main/java/com/sendgrid/constant/ErrorMessages.java @@ -0,0 +1,10 @@ +package com.sendgrid.constant; + + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ErrorMessages { + public static final String EMPTY_STRING = "'%s' can not be null or empty"; + public static final String DEFAULT_REST_CLIENT = "Sending API request using default '%s' RestClient"; +} diff --git a/src/main/java/com/sendgrid/converter/Promoter.java b/src/main/java/com/sendgrid/converter/Promoter.java new file mode 100644 index 00000000..d23ea6bf --- /dev/null +++ b/src/main/java/com/sendgrid/converter/Promoter.java @@ -0,0 +1,61 @@ +package com.sendgrid.converter; + + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +public class Promoter { + + /** + * Create a @see java.net.URI from a string + * + * @param url url to convert + * @return built @see java.net.URI + */ + public static URI uriFromString(final String url) { + try { + return new URI(url); + } catch (URISyntaxException | NullPointerException e) { + return null; + } + } + + /** + * Create a list from a single element. + * + * @param one the single element + * @param type of the element + * @return List containing the single element + */ + public static List listOfOne(final T one) { + List list = new ArrayList<>(); + list.add(one); + return list; + } + + /** + * Convert a string to a enum type. + * + * @param value string value + * @param values enum values + * @param enum type + * @return converted enum if able to convert; null otherwise + */ + public static > T enumFromString(final String value, final T[] values) { + if (value == null) { + return null; + } + + for (T v : values) { + if (v.toString().equalsIgnoreCase(value)) { + return v; + } + } + + return null; + } + + +} diff --git a/src/main/java/com/sendgrid/exception/ApiConnectionException.java b/src/main/java/com/sendgrid/exception/ApiConnectionException.java new file mode 100644 index 00000000..1e45b490 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/ApiConnectionException.java @@ -0,0 +1,15 @@ +package com.sendgrid.exception; + +public class ApiConnectionException extends SendgridException { + + private static final long serialVersionUID = 6354388724599793830L; + + public ApiConnectionException(final String message) { + super(message); + } + + public ApiConnectionException(final String message, final Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/sendgrid/exception/ApiErrorResponse.java b/src/main/java/com/sendgrid/exception/ApiErrorResponse.java new file mode 100644 index 00000000..6e887b32 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/ApiErrorResponse.java @@ -0,0 +1,29 @@ +package com.sendgrid.exception; + +import com.sun.net.httpserver.Headers; +import lombok.Getter; +import org.apache.http.Header; + +import java.util.Map; + +public class ApiErrorResponse extends RuntimeException { + @Getter + private Integer statusCode; + @Getter + private String statusMessage; + @Getter + private Object error; + @Getter + private Header[] headers; + + public ApiErrorResponse(Integer statusCode, String statusMessage, Object error, Header[] headers) { + super(statusMessage); + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.error = error; + this.headers = headers; + } + public ApiErrorResponse() { + super(); + } +} diff --git a/src/main/java/com/sendgrid/exception/ApiException.java b/src/main/java/com/sendgrid/exception/ApiException.java new file mode 100644 index 00000000..b1748ba7 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/ApiException.java @@ -0,0 +1,89 @@ +package com.sendgrid.exception; + +import java.util.Map; + +public class ApiException extends SendgridException { + + private static final long serialVersionUID = -3228320166955630014L; + + private final Integer code; + private final String moreInfo; + private final Integer status; + private final Map details; + + /** + * Create a new API Exception. + * + * @param message exception message + */ + public ApiException(final String message) { + this(message, null, null, null, null); + } + + /** + * Create a new API Exception. + * + * @param message exception message + * @param cause cause of the exception + */ + public ApiException(final String message, final Throwable cause) { + this(message, null, null, null, cause); + } + + /** + * Create a new API Exception. + * + * @param message exception message + * @param status status code + */ + public ApiException(final String message, final Integer status) { + this(message, null, null, status, null); + } + + /** + * Create a new API Exception. + * + * @param message exception message + * @param code exception code + * @param moreInfo more information if available + * @param status status code + * @param cause cause of the exception* @param cause + */ + public ApiException(final String message, final Integer code, final String moreInfo, final Integer status, + final Throwable cause) { + super(message, cause); + this.code = code; + this.moreInfo = moreInfo; + this.status = status; + this.details = null; + } + + /** + * Create a new API Exception. + * + * @param restException the rest exception + */ + public ApiException(final RestException restException) { + super(restException.getMessage(), null); + this.code = restException.getCode(); + this.moreInfo = restException.getMoreInfo(); + this.status = restException.getStatus(); + this.details = restException.getDetails(); + } + + public Integer getCode() { + return code; + } + + public String getMoreInfo() { + return moreInfo; + } + + public Integer getStatusCode() { + return status; + } + + public Map getDetails() { + return details; + } +} diff --git a/src/main/java/com/sendgrid/exception/AuthenticationException.java b/src/main/java/com/sendgrid/exception/AuthenticationException.java new file mode 100644 index 00000000..73112934 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/AuthenticationException.java @@ -0,0 +1,10 @@ +package com.sendgrid.exception; + +public class AuthenticationException extends SendgridException { + + private static final long serialVersionUID = -7779574072471080781L; + + public AuthenticationException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/sendgrid/exception/InvalidRequestException.java b/src/main/java/com/sendgrid/exception/InvalidRequestException.java new file mode 100644 index 00000000..ca173a35 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/InvalidRequestException.java @@ -0,0 +1,19 @@ +package com.sendgrid.exception; + +public class InvalidRequestException extends RuntimeException { + public InvalidRequestException() { + super(); + } + + public InvalidRequestException(String message) { + super(message); + } + + public InvalidRequestException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidRequestException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/sendgrid/exception/RestException.java b/src/main/java/com/sendgrid/exception/RestException.java new file mode 100644 index 00000000..65e98149 --- /dev/null +++ b/src/main/java/com/sendgrid/exception/RestException.java @@ -0,0 +1,82 @@ +package com.sendgrid.exception; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * Twilio Exceptions. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RestException { + + private final Integer code; + private final String message; + private final String moreInfo; + private final Integer status; + private final Map details; + + /** + * Initialize a Twilio Rest Exception. + * + * @param status HTTP status code + * @param message message of exception + * @param code Twilio status code + * @param moreInfo further information, if there is any + */ + @JsonCreator + private RestException(@JsonProperty("status") final int status, @JsonProperty("message") final String message, + @JsonProperty("code") final Integer code, @JsonProperty("more_info") final String moreInfo, + @JsonProperty("details") final Map details) { + this.status = status; + this.message = message; + this.code = code; + this.moreInfo = moreInfo; + this.details = details; + } + + /** + * Build an exception from a JSON blob. + * + * @param json JSON blob + * @param objectMapper JSON reader + * @return Rest Exception as an object + */ + public static RestException fromJson(final InputStream json, final ObjectMapper objectMapper) { + // Convert all checked exception to Runtime + try { + return objectMapper.readValue(json, RestException.class); + } catch (final JsonMappingException | JsonParseException e) { + throw new ApiException(e.getMessage(), e); + } catch (final IOException e) { + throw new ApiConnectionException(e.getMessage(), e); + } + } + + public Integer getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public String getMoreInfo() { + return moreInfo; + } + + public Integer getStatus() { + return status; + } + + public Map getDetails() { + return details; + } +} diff --git a/src/main/java/com/sendgrid/exception/SendgridException.java b/src/main/java/com/sendgrid/exception/SendgridException.java new file mode 100644 index 00000000..6fc7621c --- /dev/null +++ b/src/main/java/com/sendgrid/exception/SendgridException.java @@ -0,0 +1,14 @@ +package com.sendgrid.exception; + +public abstract class SendgridException extends RuntimeException { + + private static final long serialVersionUID = 2516935680980388130L; + + public SendgridException(final String message) { + this(message, null); + } + + public SendgridException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/sendgrid/http/ApiKeyRestClient.java b/src/main/java/com/sendgrid/http/ApiKeyRestClient.java new file mode 100644 index 00000000..388ef11c --- /dev/null +++ b/src/main/java/com/sendgrid/http/ApiKeyRestClient.java @@ -0,0 +1,98 @@ +package com.sendgrid.http; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sendgrid.http.auth.AuthStrategy; +import com.sendgrid.http.auth.TokenStrategy; +import com.sendgrid.http.httpclient.HttpClient; +import com.sendgrid.http.httpclient.ApiKeyHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +public class ApiKeyRestClient { + private final String apiKey; + private final String region; + @Getter + private final ObjectMapper objectMapper; + private final AuthStrategy tokenStrategy; + @Getter + private final HttpClient httpClient; + @Getter + private final List userAgentExtensions; + private static final Logger logger = LoggerFactory.getLogger(ApiKeyRestClient.class); + + protected ApiKeyRestClient(Builder builder) { + this.apiKey = builder.apiKey; + this.region = builder.region; + this.userAgentExtensions = builder.userAgentExtensions; + this.objectMapper = new ObjectMapper(); + this.tokenStrategy = new TokenStrategy(apiKey); + this.httpClient = builder.httpClient; + } + + public Response request(final Request request) { + tokenStrategy.applyAuth(request); + if (region != null) + request.setRegion(region); + if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) { + request.setUserAgentExtensions(userAgentExtensions); + } + logRequest(request); + + return null; + } + + + public static class Builder { + private String apiKey; + private String region; + private List userAgentExtensions; + private HttpClient httpClient; + public Builder(String apiKey) { + this.apiKey = apiKey; + } + public Builder region(String region) { + this.region = region; + return this; + } + public Builder userAgentExtensions(List userAgentExtensions) { + if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) { + this.userAgentExtensions = new ArrayList<>(userAgentExtensions); + } + return this; + } + public ApiKeyRestClient build() { + if (this.httpClient == null) { + this.httpClient = new ApiKeyHttpClient(); + } + return new ApiKeyRestClient(this); + } + } + + + + public void logRequest(final Request request) { +// if (logger.isDebugEnabled()) { +// logger.debug("-- BEGIN Twilio API Request --"); +// logger.debug("request method: " + request.getMethod()); +// // TODO: URL Encode query params before logging. +// logger.debug("request URL: " + request.getUrl().toString()); +// +// final Map headerParams = request.getHeaders(); +// +// if (headerParams != null && !headerParams.isEmpty()) { +// logger.debug("header parameters: "); +// for (String key : headerParams.keySet()) { +// if (!key.toLowerCase().contains("authorization")) { +// logger.debug(key + ": " + headerParams.get(key)); +// } +// } +// } +// +// logger.debug("-- END Twilio API Request --"); +// } + } +} diff --git a/src/main/java/com/sendgrid/http/ApiResponse.java b/src/main/java/com/sendgrid/http/ApiResponse.java new file mode 100644 index 00000000..68516521 --- /dev/null +++ b/src/main/java/com/sendgrid/http/ApiResponse.java @@ -0,0 +1,42 @@ +package com.sendgrid.http; + +import lombok.Getter; + +import java.util.Map; +import java.util.StringJoiner; + +public class ApiResponse { + @Getter + private Integer statusCode; + @Getter + private String statusMessage; + @Getter + private T body; + @Getter + private Map headers; + + + public ApiResponse(int statusCode, String statusMessage, Map headers) { + this.body = null; + this.headers = headers; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + } + + public ApiResponse(int statusCode, String statusMessage, T body, Map headers) { + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.body = body; + this.headers = headers; + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(", ", ApiResponse.class.getSimpleName() + "(", ")"); + if (statusCode != null) joiner.add("statusCode=" + statusCode); + if (statusMessage != null) joiner.add("statusMessage=" + statusMessage); + if (statusMessage != null) joiner.add("body=" + body); + if (statusMessage != null) joiner.add("headers=" + headers); + return joiner.toString(); + } +} diff --git a/src/main/java/com/sendgrid/http/HttpMethod.java b/src/main/java/com/sendgrid/http/HttpMethod.java new file mode 100644 index 00000000..445924ec --- /dev/null +++ b/src/main/java/com/sendgrid/http/HttpMethod.java @@ -0,0 +1,29 @@ +package com.sendgrid.http; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.sendgrid.converter.Promoter; + +public enum HttpMethod { + GET("GET"), + POST("POST"), + PUT("PUT"), + DELETE("DELETE"), + PATCH("PATCH"), + HEAD("HEAD"), + OPTIONS("OPTIONS"); + + private final String method; + + HttpMethod(final String method) { + this.method = method; + } + + public String toString() { + return method; + } + + @JsonCreator + public static HttpMethod forValue(final String value) { + return Promoter.enumFromString(value, HttpMethod.values()); + } +} diff --git a/src/main/java/com/sendgrid/http/Request.java b/src/main/java/com/sendgrid/http/Request.java new file mode 100644 index 00000000..181787e2 --- /dev/null +++ b/src/main/java/com/sendgrid/http/Request.java @@ -0,0 +1,92 @@ +package com.sendgrid.http; + +import com.sendgrid.util.Utility; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +public class Request { + @Getter + private final HttpMethod method; + @Getter + private final String url; + @Getter + private String body; + @Getter + private Map headers; + @Getter + @Setter + private String region; + @Getter + @Setter + private List userAgentExtensions; + + public void addHeader(String key, String value) { + if (value == null || value.equals("null") || key == null) return; + headers.put(key, value); + } + + private Request(Builder builder) { + this.method = builder.method; + this.body = builder.body; + this.headers = builder.headers; + + String baseUrl = builder.url; + baseUrl = Utility.buildWithPathParams(baseUrl, builder.pathParams); + baseUrl = Utility.buildWithQueryParams(baseUrl, builder.queryParams); + this.url = baseUrl; + } + + public static class Builder { + private String url; + private HttpMethod method; + private Map headers = new HashMap<>(); + private Map> queryParams = new HashMap<>(); + private Map pathParams = new HashMap<>(); + private String body; + + public Builder(HttpMethod method, String url) { + this.method = method; + this.url = url; + } + + public Builder addPathParam(String key, String value) { + this.pathParams.put(key, value); + return this; + } + + public Builder addHeaderParams(String key, String value) { + headers.put(key, value); + return this; + } + + /* + * Sendgrid follows Resource Query Language(RQL) for Query parameters instead of following standard query parameters. + * limit: If limit occurs as query param in open api spec + * offset: If offset occurs as query param in open api spec + * query: If there is query parameter apart from limit and offset. + * It will be the responsibility of the client to build a compound query, encode it and pass it as a query parameter in the query field. + */ + public Builder addQueryParams(String key, String value) { + if (!queryParams.containsKey(key)) { + queryParams.put(key, new ArrayList()); + } + queryParams.get(key).add(value); + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Request build() { + return new Request(this); + } + } +} diff --git a/src/main/java/com/sendgrid/http/Response.java b/src/main/java/com/sendgrid/http/Response.java new file mode 100644 index 00000000..289d9198 --- /dev/null +++ b/src/main/java/com/sendgrid/http/Response.java @@ -0,0 +1,120 @@ +package com.sendgrid.http; + +import com.sendgrid.exception.ApiException; +import org.apache.http.Header; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Scanner; + +public class Response { + + private final InputStream stream; + private String content; + private final int statusCode; + private final Header[] headers; + + /** + * Create a Response from content string and status code. + * + * @param content content string + * @param statusCode status code + */ + public Response(final String content, final int statusCode) { + this(content, statusCode, null); + } + + /** + * Create a Response from content string, status code, and headers. + * + * @param content content string + * @param statusCode status code + * @param headers headers + */ + public Response(final String content, final int statusCode, final Header[] headers) { + this.stream = null; + this.content = content; + this.statusCode = statusCode; + this.headers = headers; + } + + /** + * Create a Response from input stream and status code. + * + * @param stream input stream + * @param statusCode status code + */ + public Response(final InputStream stream, final int statusCode) { + this(stream, statusCode, null); + } + + /** + * Create a Response from input stream, status code, and headers. + * + * @param stream input stream + * @param statusCode status code + * @param headers headers + */ + public Response(final InputStream stream, final int statusCode, final Header[] headers) { + this.stream = stream; + this.content = null; + this.statusCode = statusCode; + this.headers = headers; + } + + /** + * Get the the content of the response. + * + *

+ * If there is a content string, that will be returned. + * Otherwise, will get content from input stream + *

+ * + * @return the content string + */ + public String getContent() { + if (content != null) { + return content; + } + + if (stream != null) { + Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A"); + + if (!scanner.hasNext()) { + return ""; + } + + content = scanner.next(); + scanner.close(); + + return content; + } + + return ""; + } + + /** + * Get response data as stream. + * + * @return the response data as a stream + */ + public InputStream getStream() { + if (stream != null) { + return stream; + } + try { + return new ByteArrayInputStream(content.getBytes("utf-8")); + } catch (final UnsupportedEncodingException e) { + throw new ApiException("UTF-8 encoding not supported", e); + } + } + + public int getStatusCode() { + return statusCode; + } + + public Header[] getHeaders() { + return headers; + } +} diff --git a/src/main/java/com/sendgrid/http/auth/AuthStrategy.java b/src/main/java/com/sendgrid/http/auth/AuthStrategy.java new file mode 100644 index 00000000..800f3740 --- /dev/null +++ b/src/main/java/com/sendgrid/http/auth/AuthStrategy.java @@ -0,0 +1,7 @@ +package com.sendgrid.http.auth; + +import com.sendgrid.http.Request; + +public interface AuthStrategy { + void applyAuth(Request request); +} diff --git a/src/main/java/com/sendgrid/http/auth/BasicAuthStrategy.java b/src/main/java/com/sendgrid/http/auth/BasicAuthStrategy.java new file mode 100644 index 00000000..32ea0ccb --- /dev/null +++ b/src/main/java/com/sendgrid/http/auth/BasicAuthStrategy.java @@ -0,0 +1,18 @@ +package com.sendgrid.http.auth; + +import com.sendgrid.http.Request; +import lombok.RequiredArgsConstructor; + +import java.util.Base64; + +@RequiredArgsConstructor +public class BasicAuthStrategy implements AuthStrategy { + private final String username; + private final String password; + + @Override + public void applyAuth(Request request) { + String basicAuthValue = Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + //requestBuilder.addHeader("Authorization", "Basic " + basicAuthValue); + } +} diff --git a/src/main/java/com/sendgrid/http/auth/TokenStrategy.java b/src/main/java/com/sendgrid/http/auth/TokenStrategy.java new file mode 100644 index 00000000..9c365e5e --- /dev/null +++ b/src/main/java/com/sendgrid/http/auth/TokenStrategy.java @@ -0,0 +1,13 @@ +package com.sendgrid.http.auth; + +import com.sendgrid.http.Request; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TokenStrategy implements AuthStrategy { + private final String token; + @Override + public void applyAuth(Request request) { + request.addHeader("Authorization", "Bearer " + token); + } +} diff --git a/src/main/java/com/sendgrid/http/httpclient/ApiKeyHttpClient.java b/src/main/java/com/sendgrid/http/httpclient/ApiKeyHttpClient.java new file mode 100644 index 00000000..e7e406a8 --- /dev/null +++ b/src/main/java/com/sendgrid/http/httpclient/ApiKeyHttpClient.java @@ -0,0 +1,158 @@ +package com.sendgrid.http.httpclient; + +import com.sendgrid.constant.Config; +import com.sendgrid.constant.EnumConstants; +import com.sendgrid.http.HttpMethod; +import com.sendgrid.http.Request; +import com.sendgrid.http.Response; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.utils.HttpClientUtils; +import org.apache.http.config.SocketConfig; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeader; + +public class ApiKeyHttpClient extends HttpClient { + + protected final org.apache.http.client.HttpClient client; + + private boolean isCustomClient; + + /** + * Create a new HTTP Client. + */ + public ApiKeyHttpClient() { + this(DEFAULT_REQUEST_CONFIG); + } + + /** + * Create a new HTTP Client with a custom request config. + * + * @param requestConfig a RequestConfig. + */ + public ApiKeyHttpClient(final RequestConfig requestConfig) { + this(requestConfig, DEFAULT_SOCKET_CONFIG); + } + + /** + * Create a new HTTP Client with a custom request and socket config. + * + * @param requestConfig a RequestConfig. + * @param socketConfig a SocketConfig. + */ + public ApiKeyHttpClient(final RequestConfig requestConfig, final SocketConfig socketConfig) { + Collection headers = Arrays.asList( + new BasicHeader("X-Sendgrid-Client", "java-" + Config.VERSION), + new BasicHeader(HttpHeaders.ACCEPT, "application/json"), + new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "utf-8") + ); + + String googleAppEngineVersion = System.getProperty("com.google.appengine.runtime.version"); + boolean isGoogleAppEngine = googleAppEngineVersion != null && !googleAppEngineVersion.isEmpty(); + + org.apache.http.impl.client.HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + + if (!isGoogleAppEngine) { + clientBuilder.useSystemProperties(); + } + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setDefaultSocketConfig(socketConfig); + /* + * Example: Lets say client has one server. + * There are 4 servers on edge handling client request. + * Each request takes on an average 500ms (2 request per second) + * Total number request can be server in a second from a route: 20 * 4 * 2 (DefaultMaxPerRoute * edge servers * request per second) + */ + connectionManager.setDefaultMaxPerRoute(20); + connectionManager.setMaxTotal(100); + + client = clientBuilder + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .setDefaultHeaders(headers) + .setRedirectStrategy(this.getRedirectStrategy()) + .build(); + } + + /** + * Create a new HTTP Client using custom configuration. + * @param clientBuilder an HttpClientBuilder. + */ + public ApiKeyHttpClient(HttpClientBuilder clientBuilder) { + Collection headers = Arrays.asList( + new BasicHeader("X-Sendgrid-Client", "java-" + Config.VERSION), + new BasicHeader(HttpHeaders.ACCEPT, "application/json"), + new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "utf-8") + ); + isCustomClient = true; + + client = clientBuilder + .setDefaultHeaders(headers) + .setRedirectStrategy(this.getRedirectStrategy()) + .build(); + } + + /** + * Make a request. + * + * @param request request to make + * @return Response of the HTTP request + */ + public Response makeRequest(final Request request) { + + HttpMethod method = request.getMethod(); + RequestBuilder builder = RequestBuilder.create(method.toString()) + .setUri(request.getUrl()) + .setVersion(HttpVersion.HTTP_1_1) + .setCharset(StandardCharsets.UTF_8); + + for (Map.Entry entry : request.getHeaders().entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + + if (request.getBody() != null) { + HttpEntity entity = new StringEntity(request.getBody(), ContentType.APPLICATION_JSON); + builder.setEntity(entity); + builder.addHeader( + HttpHeaders.CONTENT_TYPE, EnumConstants.ContentType.JSON.getValue()); + + } + builder.addHeader(HttpHeaders.USER_AGENT, HttpUtility.getUserAgentString(request.getUserAgentExtensions(), isCustomClient)); + + HttpResponse response = null; + + try { + response = client.execute(builder.build()); + HttpEntity entity = response.getEntity(); + return new Response( + // Consume the entire HTTP response before returning the stream + entity == null ? null : new BufferedHttpEntity(entity).getContent(), + response.getStatusLine().getStatusCode(), + response.getAllHeaders() + ); + } catch (IOException e) { + System.out.println("Exception occurred"); + } finally { + // Ensure this response is properly closed + HttpClientUtils.closeQuietly(response); + + } + return null; + } +} diff --git a/src/main/java/com/sendgrid/http/httpclient/HttpClient.java b/src/main/java/com/sendgrid/http/httpclient/HttpClient.java new file mode 100644 index 00000000..2df808f8 --- /dev/null +++ b/src/main/java/com/sendgrid/http/httpclient/HttpClient.java @@ -0,0 +1,138 @@ +package com.sendgrid.http.httpclient; + +import com.sendgrid.http.Request; +import com.sendgrid.http.Response; +import lombok.Getter; +import lombok.Setter; +import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.SocketConfig; +import org.apache.http.impl.client.DefaultRedirectStrategy; + +// Retry Logic +public abstract class HttpClient { + + public static final int CONNECTION_TIMEOUT = 10000; + public static final int SOCKET_TIMEOUT = 30500; + public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig + .custom() + .setConnectTimeout(CONNECTION_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + .build(); + public static final SocketConfig DEFAULT_SOCKET_CONFIG = SocketConfig + .custom() + .setSoTimeout(SOCKET_TIMEOUT) + .build(); + + public static final int ANY_500 = -500; + public static final int ANY_400 = -400; + public static final int ANY_300 = -300; + public static final int ANY_200 = -200; + public static final int ANY_100 = -100; + + public static final int[] RETRY_CODES = new int[] {ANY_500}; + public static final int RETRIES = 3; + public static final long DELAY_MILLIS = 100L; + + // Default redirect strategy to not auto-redirect for any methods (empty string array). + @Getter + @Setter + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(new String[0]); + + @Getter + private Response lastResponse; + @Getter + private Request lastRequest; + + /** + * Make a request. + * + * @param request request to make + * @return Response of the HTTP request + */ + public Response reliableRequest(final Request request) { + return reliableRequest(request, RETRY_CODES, RETRIES, DELAY_MILLIS); + } + + /** + * Make a request. + * + * @param request request to make + * @param retryCodes codes used for retries + * @param retries max number of retries + * @param delayMillis delays between retries + * @return Response of the HTTP request + */ + public Response reliableRequest(final Request request, final int[] retryCodes, int retries, + final long delayMillis) { + lastRequest = request; + Response response = null; + while (retries > 0) { + response = makeRequest(request); + + if (!shouldRetry(response, retryCodes)) { + break; + } + + try { + Thread.sleep(delayMillis); + } catch (final InterruptedException e) { + // Delay failed, continue + } + + // Decrement retries + retries--; + } + + lastResponse = response; + + return response; + } + + public boolean shouldRetry(final Response response, final int[] retryCodes) { + if (response == null) { + return true; + } + + int statusCode = response.getStatusCode(); + int category = (int) Math.floor(statusCode / 100.0); + + for (final int retryCode : retryCodes) { + switch (retryCode) { + case ANY_100: + if (category == 1) { + return true; + } + break; + case ANY_200: + if (category == 2) { + return true; + } + break; + case ANY_300: + if (category == 3) { + return true; + } + break; + case ANY_400: + if (category == 4) { + return true; + } + break; + case ANY_500: + if (category == 5) { + return true; + } + break; + default: + if (statusCode == retryCode) { + return true; + } + break; + } + } + return false; + } + + public abstract Response makeRequest(final Request request); +} diff --git a/src/main/java/com/sendgrid/http/httpclient/HttpUtility.java b/src/main/java/com/sendgrid/http/httpclient/HttpUtility.java new file mode 100644 index 00000000..17ccdfec --- /dev/null +++ b/src/main/java/com/sendgrid/http/httpclient/HttpUtility.java @@ -0,0 +1,36 @@ +package com.sendgrid.http.httpclient; + +import com.sendgrid.constant.Config; +import lombok.experimental.UtilityClass; + +import java.util.List; + +@UtilityClass +class HttpUtility { + public String getUserAgentString(final List userAgentExtensions) { + StringBuilder userAgentString = new StringBuilder(); + userAgentString.append("sendgrid-java/") + .append(Config.VERSION) + .append(" (") + .append(Config.OS_NAME) + .append(" ") + .append(Config.OS_ARCH) + .append(") ") + .append("java/") + .append(Config.JAVA_VERSION); + + if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) { + userAgentExtensions.stream().forEach(userAgentExtension -> { + userAgentString.append(" "); + userAgentString.append(userAgentExtension); + }); + } + + return userAgentString.toString(); + } + + public String getUserAgentString(final List userAgentExtensions, final boolean isCustomClient) { + return isCustomClient ? getUserAgentString(userAgentExtensions) + " custom" + : getUserAgentString(userAgentExtensions); + } +} diff --git a/src/main/java/com/sendgrid/util/JsonUtil.java b/src/main/java/com/sendgrid/util/JsonUtil.java new file mode 100644 index 00000000..e30cb03b --- /dev/null +++ b/src/main/java/com/sendgrid/util/JsonUtil.java @@ -0,0 +1,92 @@ +package com.sendgrid.util; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sendgrid.exception.ApiConnectionException; +import com.sendgrid.exception.ApiException; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import java.io.InputStream; + +/** + * Utility class for JSON serialization and deserialization using Jackson. + *

+ * This class provides methods to convert Java objects to JSON strings and vice versa. + * It includes a default {@link ObjectMapper} instance for common use cases, + * while also allowing custom {@link ObjectMapper} instances for specific configurations. + **/ +public class JsonUtil { + private static final ObjectMapper defaultObjectMapper = createDefaultObjectMapper(); + + private static ObjectMapper createDefaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } + + public static String toJson(Object object) { + return toJson(object, defaultObjectMapper); + } + + /** + * Converts an object to a JSON string using Jackson ObjectMapper. + * + * @param object the object to be converted to JSON + * @param mapper the ObjectMapper instance + * @return the JSON string representation of the object + * @throws ApiException if there is a JSON processing or mapping error + */ + public static String toJson(Object object, ObjectMapper mapper) { + try { + return mapper.writeValueAsString(object); + } catch (JsonMappingException e) { + throw new ApiException(e.getMessage(), e); + } catch (JsonProcessingException e) { + throw new ApiException(e.getMessage(), e); + } + } + + public static T fromJson(InputStream json, Class clazz) { + return fromJson(json, defaultObjectMapper, clazz); + } + + /** + * Converts an InputStream to an object of the specified class using Jackson ObjectMapper. + * + * @param the type of the desired object + * @param json the InputStream containing JSON data + * @param objectMapper the ObjectMapper instance + * @param clazz the Class object of the desired type + * @return the object of type T + * @throws ApiException if there is a JSON parsing or mapping error + * @throws ApiConnectionException if there is an I/O error + */ + public static T fromJson(final InputStream json, final ObjectMapper objectMapper, final Class clazz) { + // Convert all checked exceptions to Runtime + try { + return objectMapper.readValue(json, clazz); + } catch (final JsonMappingException | JsonParseException e) { + throw new ApiException(e.getMessage(), e); + } catch (final IOException e) { + throw new ApiConnectionException(e.getMessage(), e); + } + } + + // Handle generic types such as List + public static T fromJson(InputStream json, TypeReference typeReference) { + return fromJson(json, defaultObjectMapper, typeReference); + } + + public static T fromJson(InputStream json, ObjectMapper objectMapper, TypeReference typeReference) { + try { + return objectMapper.readValue(json, typeReference); + } catch (JsonMappingException | JsonParseException e) { + throw new ApiException(e.getMessage(), e); + } catch (IOException e) { + throw new ApiConnectionException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/sendgrid/util/Matcher.java b/src/main/java/com/sendgrid/util/Matcher.java new file mode 100644 index 00000000..3b60035c --- /dev/null +++ b/src/main/java/com/sendgrid/util/Matcher.java @@ -0,0 +1,21 @@ +package com.sendgrid.util; + +public class Matcher { + /* + * responseCode: Response Code received via API call. + * responseCodePattern: response code pattern mentioned in open api spec. + */ + public static boolean matches(String responseCode, String responseCodePattern) { + // Escape all characters in the responseCodePattern except 'x' + StringBuilder regex = new StringBuilder(); + for (char ch : responseCodePattern.toCharArray()) { + if (ch == 'x') { + regex.append("\\d"); + } else { + regex.append(ch); + } + } + String regexPattern = regex.toString(); + return responseCode.matches(regexPattern); + } +} diff --git a/src/main/java/com/sendgrid/util/Utility.java b/src/main/java/com/sendgrid/util/Utility.java new file mode 100644 index 00000000..61ec6b50 --- /dev/null +++ b/src/main/java/com/sendgrid/util/Utility.java @@ -0,0 +1,38 @@ +package com.sendgrid.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sendgrid.exception.ApiConnectionException; +import com.sendgrid.exception.ApiException; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +public class Utility { + public static String buildWithPathParams(String path, Map params) { + for (Map.Entry entry : params.entrySet()) { + String placeholder = "\\{" + entry.getKey() + "\\}"; + path = path.replaceAll(placeholder, entry.getValue()); + } + return path; + } + + public static String buildWithQueryParams(String path, Map> queryParams) { + if (queryParams.isEmpty()) { + return path; + } + StringJoiner joiner = new StringJoiner("&"); + queryParams.forEach((key, values) -> { + values.forEach(value -> { + joiner.add(key + "=" + value); // In case all query parameter needs to be URL Encoded, encode value here. + }); + }); + path = path + "?" + joiner.toString(); + return path; + } +}