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
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,23 @@ jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
csv.required = false
}
}

jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.70
}
}
}
}

check.dependsOn jacocoTestCoverageVerification

tasks.register('sourcesJar', Jar) {
from sourceSets.main.allJava
archiveClassifier = 'sources'
Expand Down
179 changes: 86 additions & 93 deletions src/main/java/com/checkout/ApacheHttpClientTransport.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.checkout;

import com.checkout.accounts.AccountsFileRequest;
import com.checkout.accounts.Headers;
import com.checkout.common.AbstractFileRequest;
import com.checkout.common.CheckoutUtils;
import com.checkout.common.FileRequest;
Expand Down Expand Up @@ -34,7 +33,6 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
Expand All @@ -43,8 +41,6 @@
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.function.Supplier;

import com.google.gson.annotations.SerializedName;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.retry.Retry;
Expand Down Expand Up @@ -101,43 +97,21 @@ public CompletableFuture<Response> invoke(final ClientOperation clientOperation,
final Object requestObject,
final String idempotencyKey,
final Map<String, String> queryParams) {
return invoke(clientOperation, path, authorization, requestObject, idempotencyKey, queryParams, null);
}

@Override
public CompletableFuture<Response> invoke(final ClientOperation clientOperation,
final String path,
final SdkAuthorization authorization,
final Object requestObject,
final String idempotencyKey,
final Map<String, String> queryParams,
final IHeaders headers) {

return CompletableFuture.supplyAsync(() -> {
final HttpUriRequest request;
switch (clientOperation) {
case GET:
case GET_CSV_CONTENT:
request = new HttpGet(getRequestUrl(path));
break;
case PUT:
request = new HttpPut(getRequestUrl(path));
break;
case POST:
request = new HttpPost(getRequestUrl(path));
break;
case DELETE:
request = new HttpDelete(getRequestUrl(path));
break;
case PATCH:
request = new HttpPatch(getRequestUrl(path));
break;
case QUERY:
final List<NameValuePair> params = queryParams.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
try {
request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}
break;
default:
throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation);
}
if (idempotencyKey != null) {
request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey);
}
return performCall(authorization, requestObject, request, clientOperation);
final HttpUriRequest request = buildRequest(clientOperation, path, queryParams, idempotencyKey);
return performCall(authorization, requestObject, request, clientOperation, headers);
}, executor);
}

Expand All @@ -157,42 +131,20 @@ public Response invokeSync(final ClientOperation clientOperation,
final Object requestObject,
final String idempotencyKey,
final Map<String, String> queryParams) {
final HttpUriRequest request;
switch (clientOperation) {
case GET:
case GET_CSV_CONTENT:
request = new HttpGet(getRequestUrl(path));
break;
case PUT:
request = new HttpPut(getRequestUrl(path));
break;
case POST:
request = new HttpPost(getRequestUrl(path));
break;
case DELETE:
request = new HttpDelete(getRequestUrl(path));
break;
case PATCH:
request = new HttpPatch(getRequestUrl(path));
break;
case QUERY:
final List<NameValuePair> params = queryParams.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
try {
request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}
break;
default:
throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation);
}
if (idempotencyKey != null) {
request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey);
}

final Supplier<Response> callSupplier = () -> performCall(authorization, requestObject, request, clientOperation);
return invokeSync(clientOperation, path, authorization, requestObject, idempotencyKey, queryParams, null);
}

@Override
public Response invokeSync(final ClientOperation clientOperation,
final String path,
final SdkAuthorization authorization,
final Object requestObject,
final String idempotencyKey,
final Map<String, String> queryParams,
final IHeaders headers) {
final HttpUriRequest request = buildRequest(clientOperation, path, queryParams, idempotencyKey);

final Supplier<Response> callSupplier = () -> performCall(authorization, requestObject, request, clientOperation, headers);
return executeWithResilience4j(callSupplier);
}

Expand Down Expand Up @@ -258,13 +210,21 @@ private Response performCall(final SdkAuthorization authorization,
final Object requestBody,
final HttpUriRequest request,
final ClientOperation clientOperation) {
return performCall(authorization, requestBody, request, clientOperation, null);
}

private Response performCall(final SdkAuthorization authorization,
final Object requestBody,
final HttpUriRequest request,
final ClientOperation clientOperation,
final IHeaders customHeaders) {
log.info("{}: {}", clientOperation, request.getURI());
request.setHeader(USER_AGENT, PROJECT_NAME + "/" + getVersionFromManifest());
request.setHeader(ACCEPT, getAcceptHeader(clientOperation));
request.setHeader(AUTHORIZATION, authorization.getAuthorizationHeader());

// Check and add headers from the request object if needed
addHeadersFromRequestBody(requestBody, request);
applyHeaders(customHeaders, request);

String currentRequestId = UUID.randomUUID().toString();

Expand Down Expand Up @@ -318,19 +278,17 @@ private Response performCall(final SdkAuthorization authorization,
}

private void addHeadersFromRequestBody(final Object requestBody, final HttpUriRequest request) {
if (requestBody instanceof Headers) {
Headers headers = (Headers) requestBody;
for (Field field : Headers.class.getDeclaredFields()) {
field.setAccessible(true);
try {
Object value = field.get(headers);
if (value != null && !value.toString().isEmpty()) {
SerializedName serializedName = field.getAnnotation(SerializedName.class);
String headerName = serializedName != null ? serializedName.value() : field.getName();
request.setHeader(headerName, value.toString());
}
} catch (IllegalAccessException ignored) {}
}
if (requestBody instanceof IHeaders) {
applyHeaders((IHeaders) requestBody, request);
}
}

private void applyHeaders(final IHeaders headers, final HttpUriRequest request) {
if (headers == null) {
return;
}
for (Map.Entry<String, String> entry : headers.getHeaders().entrySet()) {
request.setHeader(entry.getKey(), entry.getValue());
}
}

Expand Down Expand Up @@ -363,12 +321,9 @@ private Response handleException(Exception e, String errorMessage) {
}

private Header[] sanitiseHeaders(final Header[] headers) {
// TODO: discuss whether Cko-Idempotency-Key should also be filtered — it is a per-request
// unique identifier (not a credential), but filtering it reduces exposure in INFO logs.
// Uncomment the line below once agreed.
// .filter(it -> !it.getName().equalsIgnoreCase(CKO_IDEMPOTENCY_KEY))
return Arrays.stream(headers)
.filter(it -> !it.getName().equals(AUTHORIZATION))
.filter(it -> !it.getName().equalsIgnoreCase("Signature"))
.toArray(Header[]::new);
}

Expand All @@ -388,6 +343,44 @@ private String getAcceptHeader(final ClientOperation clientOperation) {
}
}

private HttpUriRequest buildRequest(final ClientOperation clientOperation, final String path, final Map<String, String> queryParams, final String idempotencyKey) {
final HttpUriRequest request;
switch (clientOperation) {
case GET:
case GET_CSV_CONTENT:
request = new HttpGet(getRequestUrl(path));
break;
case PUT:
request = new HttpPut(getRequestUrl(path));
break;
case POST:
request = new HttpPost(getRequestUrl(path));
break;
case DELETE:
request = new HttpDelete(getRequestUrl(path));
break;
case PATCH:
request = new HttpPatch(getRequestUrl(path));
break;
case QUERY:
final List<NameValuePair> params = queryParams.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
try {
request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}
break;
default:
throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation);
}
if (idempotencyKey != null) {
request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey);
}
return request;
}

private String getRequestUrl(final String path) {
try {
return baseUri.resolve(path).toURL().toString();
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/checkout/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface ApiClient {

<T extends HttpMetadata> CompletableFuture<T> postAsync(String path, SdkAuthorization authorization, Class<T> responseType, Object request, String idempotencyKey);

<T extends HttpMetadata> CompletableFuture<T> postAsync(String path, SdkAuthorization authorization, Class<T> responseType, Object request, String idempotencyKey, IHeaders headers);

<T extends HttpMetadata> CompletableFuture<T> patchAsync(String path, SdkAuthorization authorization, Type type, Object request, String idempotencyKey);

<T extends HttpMetadata> CompletableFuture<T> postAsync(String path, SdkAuthorization authorization, Type responseType, Object request, String idempotencyKey);
Expand Down Expand Up @@ -47,6 +49,8 @@ public interface ApiClient {

<T extends HttpMetadata> T post(String path, SdkAuthorization authorization, Class<T> responseType, Object request, String idempotencyKey);

<T extends HttpMetadata> T post(String path, SdkAuthorization authorization, Class<T> responseType, Object request, String idempotencyKey, IHeaders headers);

<T extends HttpMetadata> T post(String path, SdkAuthorization authorization, Type responseType, Object request, String idempotencyKey);

EmptyResponse delete(String path, SdkAuthorization authorization);
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/checkout/ApiClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ public <T extends HttpMetadata> CompletableFuture<T> postAsync(final String path
);
}

@Override
public <T extends HttpMetadata> CompletableFuture<T> postAsync(final String path, final SdkAuthorization authorization, final Class<T> responseType, final Object request, final String idempotencyKey, final IHeaders headers) {
validateParams(PATH, path, AUTHORIZATION, authorization);
return executeAsyncOrSync(
() -> post(path, authorization, responseType, request, idempotencyKey, headers),
() -> sendRequestAsync(POST, path, authorization, request, idempotencyKey, responseType, headers)
);
}

@Override
public <T extends HttpMetadata> CompletableFuture<T> postAsync(final String path, final SdkAuthorization authorization, final Type responseType, final Object request, final String idempotencyKey) {
validateParams(PATH, path, AUTHORIZATION, authorization);
Expand Down Expand Up @@ -236,6 +245,12 @@ private <T extends HttpMetadata> CompletableFuture<T> sendRequestAsync(final Cli
.thenApply(response -> deserialize(response, responseType));
}

private <T extends HttpMetadata> CompletableFuture<T> sendRequestAsync(final ClientOperation clientOperation, final String path, final SdkAuthorization authorization, final Object request, final String idempotencyKey, final Type responseType, final IHeaders headers) {
return transport.invoke(clientOperation, path, authorization, request, idempotencyKey, null, headers)
.thenApply(this::errorCheck)
.thenApply(response -> deserialize(response, responseType));
}

private Response errorCheck(final Response response) {
if (!CheckoutUtils.isSuccessHttpStatusCode(response.getStatusCode())) {
Map<String, Object> errorDetails = null;
Expand Down Expand Up @@ -335,6 +350,12 @@ public <T extends HttpMetadata> T post(final String path, final SdkAuthorization
return sendRequestSync(POST, path, authorization, request, idempotencyKey, responseType);
}

@Override
public <T extends HttpMetadata> T post(final String path, final SdkAuthorization authorization, final Class<T> responseType, final Object request, final String idempotencyKey, final IHeaders headers) {
validateParams(PATH, path, AUTHORIZATION, authorization);
return sendRequestSync(POST, path, authorization, request, idempotencyKey, responseType, headers);
}

@Override
public <T extends HttpMetadata> T post(final String path, final SdkAuthorization authorization, final Type responseType, final Object request, final String idempotencyKey) {
validateParams(PATH, path, AUTHORIZATION, authorization);
Expand Down Expand Up @@ -411,4 +432,10 @@ private <T extends HttpMetadata> T sendRequestSync(final ClientOperation clientO
return deserialize(checkedResponse, responseType);
}

private <T extends HttpMetadata> T sendRequestSync(final ClientOperation clientOperation, final String path, final SdkAuthorization authorization, final Object request, final String idempotencyKey, final Type responseType, final IHeaders headers) {
final Response response = transport.invokeSync(clientOperation, path, authorization, request, idempotencyKey, null, headers);
final Response checkedResponse = errorCheck(response);
return deserialize(checkedResponse, responseType);
}

}
7 changes: 7 additions & 0 deletions src/main/java/com/checkout/CheckoutApi.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.checkout;

import com.checkout.accounts.AccountsClient;
import com.checkout.agenticcommerce.AgenticCommerceClient;
import com.checkout.balances.BalancesClient;
import com.checkout.customers.CustomersClient;
import com.checkout.disputes.DisputesClient;
Expand Down Expand Up @@ -102,4 +103,10 @@ public interface CheckoutApi extends CheckoutApmApi {

StandaloneAccountUpdaterClient standaloneAccountUpdaterClient();

/**
* Returns the client for the Agentic Commerce Protocol (Beta).
* Use to create delegated payment tokens for AI-agent-initiated payments.
*/
AgenticCommerceClient agenticCommerceClient();

}
Loading
Loading