Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 142 additions & 14 deletions src/main/java/com/adyen/httpclient/AdyenHttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
import com.adyen.Config;
import com.adyen.constants.ApiConstants;
import com.adyen.model.RequestOptions;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
Expand All @@ -55,10 +58,13 @@
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.URIBuilder;
Expand Down Expand Up @@ -201,10 +207,36 @@
HttpUriRequestBase httpRequest =
createRequest(
endpoint, requestBody, config, isApiKeyRequired, requestOptions, httpMethod, params);
return executeRequest(httpclient, httpRequest);
}

// Execute request with a custom response handler
AdyenResponse response = httpclient.execute(httpRequest, new AdyenResponseHandler());
@Override
public String requestMultipart(
String endpoint,
Map<String, Object> formParams,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException {
CloseableHttpClient httpclient = getOrCreateHttpClient(config);
HttpEntity multipartEntity = createMultipartEntity(formParams);
HttpUriRequestBase httpRequest =
createRequest(
endpoint,
new RequestBody(multipartEntity, multipartEntity.getContentType()),
config,
isApiKeyRequired,
requestOptions,
httpMethod,
params);
return executeRequest(httpclient, httpRequest);
}

private String executeRequest(CloseableHttpClient httpclient, HttpUriRequestBase httpRequest)
throws IOException, HTTPClientException {
AdyenResponse response = httpclient.execute(httpRequest, new AdyenResponseHandler());
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new HTTPClientException(
response.getStatus(), "HTTP Exception", response.getHeaders(), response.getBody());
Expand Down Expand Up @@ -235,8 +267,51 @@
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException {
HttpEntity requestEntity =
requestBody == null || requestBody.isEmpty()
? null
: new StringEntity(requestBody, Charset.forName(CHARSET));

Check warning on line 273 in src/main/java/com/adyen/httpclient/AdyenHttpClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace Charset.forName() call with StandardCharsets.UTF_8

See more on https://sonarcloud.io/project/issues?id=Adyen_adyen-java-api-library&issues=AaA4gVO9i6fQBG-Zel3f&open=AaA4gVO9i6fQBG-Zel3f&pullRequest=2052
return createRequest(
endpoint,
new RequestBody(requestEntity, APPLICATION_JSON_TYPE),
config,
isApiKeyRequired,
requestOptions,
httpMethod,
params);
}

HttpUriRequestBase createMultipartRequest(
String endpoint,
Map<String, Object> formParams,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException, IOException {
HttpEntity multipartEntity = createMultipartEntity(formParams);
return createRequest(
endpoint,
new RequestBody(multipartEntity, multipartEntity.getContentType()),
config,
isApiKeyRequired,
requestOptions,
httpMethod,
params);
}

private HttpUriRequestBase createRequest(
String endpoint,
RequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException {
HttpUriRequestBase httpRequest =
createHttpRequestBase(createUri(endpoint, params), requestBody, httpMethod);
createHttpRequestBase(createUri(endpoint, params), requestBody.entity, httpMethod);

RequestConfig.Builder builder = RequestConfig.custom();

Expand All @@ -256,15 +331,19 @@
httpRequest.setConfig(builder.build());

setAuthentication(httpRequest, isApiKeyRequired, config);
setHeaders(config, requestOptions, httpRequest);
setHeaders(config, requestOptions, httpRequest, requestBody.contentType);

return httpRequest;
}

private void setHeaders(

Check failure on line 339 in src/main/java/com/adyen/httpclient/AdyenHttpClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Adyen_adyen-java-api-library&issues=AaA4oZ3r0Sba2W3b_eLj&open=AaA4oZ3r0Sba2W3b_eLj&pullRequest=2052
Config config, RequestOptions requestOptions, HttpUriRequestBase httpUriRequest) {
Config config,
RequestOptions requestOptions,
HttpUriRequestBase httpUriRequest,
String contentType) {

setContentType(httpUriRequest, APPLICATION_JSON_TYPE);
setContentType(httpUriRequest, contentType);
boolean isMultipart = contentType != null && contentType.startsWith("multipart/");
httpUriRequest.addHeader(ACCEPT_CHARSET, CHARSET);

String applicationName = config.getApplicationName();
Expand Down Expand Up @@ -292,18 +371,24 @@
}

if (requestOptions.getAdditionalServiceHeaders() != null) {
requestOptions.getAdditionalServiceHeaders().forEach(httpUriRequest::addHeader);
requestOptions
.getAdditionalServiceHeaders()
.forEach(
(name, value) -> {
if (CONTENT_TYPE.equalsIgnoreCase(name)) {
if (!isMultipart) {
httpUriRequest.setHeader(name, value);
}
} else {
httpUriRequest.addHeader(name, value);
}
});
}
Comment thread
poojah-adyen marked this conversation as resolved.
}
}

private HttpUriRequestBase createHttpRequestBase(
URI endpoint, String requestBody, ApiConstants.HttpMethod httpMethod) {
StringEntity requestEntity = null;
if (requestBody != null && !requestBody.isEmpty()) {
requestEntity = new StringEntity(requestBody, Charset.forName(CHARSET));
}

URI endpoint, HttpEntity requestEntity, ApiConstants.HttpMethod httpMethod) {
switch (httpMethod) {
case GET:
return new HttpGet(endpoint);
Expand All @@ -321,6 +406,49 @@
}
}

HttpEntity createMultipartEntity(Map<String, Object> formParams) throws IOException {
MultipartEntityBuilder builder =
MultipartEntityBuilder.create()
.setContentType(ContentType.create("multipart/form-data"))
.setCharset(StandardCharsets.UTF_8);
for (Map.Entry<String, Object> entry : formParams.entrySet()) {
addMultipartPart(builder, entry.getKey(), entry.getValue());
}
return builder.build();
}
Comment thread
poojah-adyen marked this conversation as resolved.

private void addMultipartPart(MultipartEntityBuilder builder, String name, Object value)
throws IOException {
if (value == null) {
return;
}
if (value instanceof Iterable<?>) {
for (Object element : (Iterable<?>) value) {
addMultipartPart(builder, name, element);
}
return;
}
if (value instanceof File) {
File file = (File) value;
String mimeType = Files.probeContentType(file.toPath());
ContentType contentType =
mimeType == null ? ContentType.APPLICATION_OCTET_STREAM : ContentType.create(mimeType);
builder.addBinaryBody(name, file, contentType, file.getName());
return;
}
builder.addTextBody(name, String.valueOf(value), ContentType.create("text/plain", CHARSET));
}

private static final class RequestBody {
private final HttpEntity entity;
private final String contentType;

private RequestBody(HttpEntity entity, String contentType) {
this.entity = entity;
this.contentType = contentType;
}
}

private URI createUri(String endpoint, Map<String, String> params) throws HTTPClientException {
try {
URIBuilder uriBuilder = new URIBuilder(endpoint);
Expand Down Expand Up @@ -406,7 +534,7 @@

/** Sets the Content-Type header on the request. */
private void setContentType(HttpUriRequest httpUriRequest, String contentType) {
httpUriRequest.addHeader(CONTENT_TYPE, contentType);
httpUriRequest.setHeader(CONTENT_TYPE, contentType);
}

/** Sets the X-API-Key header on the request. */
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/adyen/httpclient/ClientInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,36 @@ String request(
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException;

/**
* Sends a multipart/form-data request with the specified method, authentication, request options,
* and query string parameters.
*
* <p>The default implementation preserves binary compatibility for custom HTTP client
* implementations that do not support multipart requests.
*
* @param endpoint the full URL of the API endpoint
* @param formParams multipart form fields, including files and text values
* @param config the client configuration
* @param isApiKeyRequired whether API key authentication is mandatory
* @param requestOptions additional request options (idempotency key, custom headers)
* @param httpMethod the HTTP method (POST or PATCH)
* @param params query string parameters appended to the URL
* @return the JSON response body
* @throws IOException if a network error occurs
* @throws HTTPClientException if the server returns a non-2xx status code
*/
default String requestMultipart(
String endpoint,
Map<String, Object> formParams,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException {
throw new UnsupportedOperationException(
"Multipart requests are not supported by this HTTP client. "
+ "Custom ClientInterface implementations must override requestMultipart().");
}
}
60 changes: 52 additions & 8 deletions src/main/java/com/adyen/service/resource/Resource.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,55 @@ public String request(
throws ApiException, IOException {
ClientInterface clientInterface = service.getClient().getHttpClient();
Config config = service.getClient().getConfig();
return executeRequest(
() ->
clientInterface.request(
resolve(pathParams),
json,
config,
service.isApiKeyRequired(),
requestOptions,
httpMethod,
queryString));
}

/**
* Sends a multipart/form-data request with optional path and query string parameters.
*
* @param formParams multipart form fields
* @param requestOptions additional request options
* @param httpMethod HTTP method
* @param pathParams parameters used to resolve path placeholders
* @param queryString query string parameters
* @return JSON response
* @throws ApiException when an API error is returned
* @throws IOException when an unexpected error occurs
*/
public String requestMultipart(
Map<String, Object> formParams,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> pathParams,
Map<String, String> queryString)
throws ApiException, IOException {
ClientInterface clientInterface = service.getClient().getHttpClient();
Config config = service.getClient().getConfig();
return executeRequest(
() ->
clientInterface.requestMultipart(
resolve(pathParams),
formParams,
config,
service.isApiKeyRequired(),
requestOptions,
httpMethod,
queryString));
}

private String executeRequest(RequestCall requestCall) throws ApiException, IOException {
ApiException apiException;
try {
return clientInterface.request(
resolve(pathParams),
json,
config,
service.isApiKeyRequired(),
requestOptions,
httpMethod,
queryString);
return requestCall.execute();
} catch (HTTPClientException e) {
try {
// build ApiException
Expand All @@ -145,6 +184,11 @@ public String request(
throw apiException;
}

@FunctionalInterface
private interface RequestCall {
String execute() throws IOException, HTTPClientException;
}

private String resolve(Map<String, String> params) {
if (endpoint == null || params == null || endpoint.isEmpty() || params.isEmpty()) {
return endpoint;
Expand Down
Loading