Customizing the Java SDK

Ask about this Page
Copy for LLM
View as Markdown

Serialization

The SDK uses Jackson to serialize and deserialize JSON. The default configured ObjectMapper uses some modules to correctly work with commercetools APIs. For more information, see JsonUtils.createObjectMapper(ModuleOptions).

Customization

To allow customization of ObjectMapper, the SDK uses ServiceLoader for ModuleSupplier. To register the supplied modules, add the resources/META-INF/services/io.vrap.rmf.base.client.utils.json.ModuleSupplier file to your project and include the fully qualified class name of the module supplier that you want to use.
Register the custom ModuleSupplierjava
io.vrap.rmf.base.client.utils.json.ModuleSupplier:
com.commercetools.api.json.ApiModuleSupplier
Implement a custom ApiModulejava
package com.commercetools.api.json;

import java.util.Optional;

import com.commercetools.api.models.cart.ReplicaCartDraft;
import com.commercetools.api.models.product.AttributeImpl;
import com.commercetools.api.models.review.Review;
import com.commercetools.api.models.type.FieldContainerImpl;
import com.commercetools.api.json.AttributeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;

import io.vrap.rmf.base.client.utils.json.modules.ModuleOptions;

/**
 * Module to configure the default jackson {@link com.fasterxml.jackson.databind.ObjectMapper} e.g. to deserialize attributes and custom fields
 */
public class ApiModule extends SimpleModule {
    private static final long serialVersionUID = 0L;

    public ApiModule(ModuleOptions options) {
        boolean attributeAsDateString = Boolean.parseBoolean(
            Optional.ofNullable(options.getOption(ApiModuleOptions.DESERIALIZE_DATE_ATTRIBUTE_AS_STRING))
                    .orElse(System.getProperty(ApiModuleOptions.DESERIALIZE_DATE_ATTRIBUTE_AS_STRING)));
        boolean customFieldAsDateString = Boolean
                .parseBoolean(Optional.ofNullable(options.getOption(ApiModuleOptions.DESERIALIZE_DATE_FIELD_AS_STRING))
                        .orElse(System.getProperty(ApiModuleOptions.DESERIALIZE_DATE_FIELD_AS_STRING)));
        addDeserializer(AttributeImpl.class, new AttributeDeserializer(attributeAsDateString));
        addDeserializer(FieldContainerImpl.class, new CustomFieldDeserializer(customFieldAsDateString));
        setMixInAnnotation(Review.class, ReviewMixin.class);
        setMixInAnnotation(ReplicaCartDraft.class, ReplicaCartDraftMixin.class);
    }
}

DateTime attributes

When using Date, Time and DateTime types for Attributes or Custom Fields the SDK deserializes them as LocalDate, LocalTime and ZonedDateTime.
To deserialize them as a String, configure ObjectMapper with ModuleOptions. ApiModule also loads the configuration options by using System.getProperty(String); for example, commercetools.deserializeDateAttributeAsString).
Deserialize Date, Time, and DateTime Attributes as stringsjava
ApiModuleOptions options = ApiModuleOptions
  .of()
  .withDateAttributeAsString(true)
  .withDateCustomFieldAsString(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);

ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withSerializer(ResponseSerializer.of(mapper))
  .build("test");

ProductVariant variant = mapper.readValue(
  stringFromResource("attributes.json"),
  ProductVariant.class
);

assertThat(variant.getAttributes()).isNotEmpty();

Map<String, Attribute> attributes = variant.withProductVariant(
  AttributeAccessor::asMap
);

assertThat(attributes.get("date").getValue())
  .isInstanceOfSatisfying(
    String.class,
    localDate -> assertThat(localDate).isEqualTo("2020-01-01")
  );
assertThat(attributes.get("time").getValue())
  .isInstanceOfSatisfying(
    String.class,
    localTime -> assertThat(localTime).isEqualTo("13:15:00.123")
  );
assertThat(attributes.get("datetime").getValue())
  .isInstanceOfSatisfying(
    String.class,
    dateTime -> assertThat(dateTime).isEqualTo("2020-01-01T13:15:00.123Z")
  );
assertThat(attributes.get("date").withAttribute(AttributeAccessor::asDate))
  .isInstanceOfSatisfying(
    LocalDate.class,
    localDate -> assertThat(localDate).isEqualTo("2020-01-01")
  );
assertThat(attributes.get("time").withAttribute(AttributeAccessor::asTime))
  .isInstanceOfSatisfying(
    LocalTime.class,
    localTime -> assertThat(localTime).isEqualTo("13:15:00.123")
  );
assertThat(
  attributes.get("datetime").withAttribute(AttributeAccessor::asDateTime)
)
  .isInstanceOfSatisfying(
    ZonedDateTime.class,
    dateTime -> assertThat(dateTime).isEqualTo("2020-01-01T13:15:00.123Z")
  );

assertThat(attributes.get("set-date").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(attributes.get("set-time").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(attributes.get("set-datetime").getValue())
  .asList()
  .first()
  .isInstanceOf(String.class);
assertThat(
  attributes.get("set-date").withAttribute(AttributeAccessor::asSetDate)
)
  .asList()
  .first()
  .isInstanceOf(LocalDate.class);
assertThat(
  attributes.get("set-time").withAttribute(AttributeAccessor::asSetTime)
)
  .asList()
  .first()
  .isInstanceOf(LocalTime.class);
assertThat(
  attributes.get("set-datetime").withAttribute(AttributeAccessor::asSetDateTime)
)
  .asList()
  .first()
  .isInstanceOf(ZonedDateTime.class);

Number attributes

When using number types for Attributes or Custom Fields, the SDK automatically deserializes them as Long if they have no fractional part. To always deserialize them as Double, configure ObjectMapper with ApiModuleOptions:
Deserialize number Attributes as Doublejava
ApiModuleOptions options = ApiModuleOptions
  .of()
  .withAttributeNumberAsDouble(true)
  .withCustomFieldNumberAsDouble(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);

You can also set these options using system properties:

  • commercetools.deserializeAttributeNumberAsDouble
  • commercetools.deserializeCustomFieldNumberAsDouble

Deserialize Attributes as JsonNode

If the automatic deserialization of Attributes or Custom Fields is not needed, you can configure the SDK to deserialize them as a JsonNode instead:
Deserialize Attributes as JsonNodejava
ApiModuleOptions options = ApiModuleOptions
  .of()
  .withAttributeAsJsonNode(true)
  .withCustomFieldAsJsonNode(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);

You can also set these options using system properties:

  • commercetools.deserializeAttributeAsJsonNode
  • commercetools.deserializeCustomFieldAsJsonNode

Authentication

The Java SDK supports multiple authentication flows. The defaultClient() method on ApiRootBuilder configures the client credentials flow by default.

Static token

When you want to use an existing token (for example, one provided by an external OAuth service), use the withStaticTokenFlow() method:
Configure a client with a static tokenjava
AuthenticationToken token = AuthenticationToken
  .builder()
  .accessToken("your-existing-token")
  .build();

ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withStaticTokenFlow(token)
  .build("my-project-key");

Anonymous session and refresh token flow

The withAnonymousRefreshFlow() method configures a stack of token providers that first try to retrieve a token from a TokenStorage. If no token exists, it requests one using the anonymous token flow. If the token is expired, the RefreshFlowTokenSupplier tries to refresh it.
This can be combined with the GlobalCustomerPasswordTokenSupplier to request a customer-bound token and save it in the TokenStorage.
Configure the anonymous refresh token flowjava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withAnonymousRefreshFlow(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1,
    tokenStorage
  )
  .build("my-project-key");

Anonymous session flow

The AnonymousSessionTokenSupplier requests an anonymous token without a fallback to a refresh token flow. Configure it with withAnonymousSessionFlow():
Configure the anonymous session flowjava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withAnonymousSessionFlow(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    "anonymous-id"
  )
  .build("my-project-key");

Password flow

Use withGlobalCustomerPasswordFlow() to configure a client with a token issued to a specific customer using the GlobalCustomerPasswordTokenSupplier:
Configure the customer password flowjava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .withApiBaseUrl(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
  .withGlobalCustomerPasswordFlow(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    "customer@example.com",
    "customer-password",
    "my-project-key"
  )
  .build("my-project-key");

Token introspection

Token introspection is not available through the SDK client directly. You must call the introspection endpoint using a raw ApiHttpClient:
Call the token introspection endpointjava
ClientCredentials credentials = ClientCredentials.of()
  .withClientId("your-client-id")
  .withClientSecret("your-client-secret")
  .build();

// Obtain a token
ClientCredentialsTokenSupplier tokenSupplier = new ClientCredentialsTokenSupplier(
  credentials.getClientId(),
  credentials.getClientSecret(),
  "",
  ServiceRegion.GCP_EUROPE_WEST1.getOAuthTokenUrl(),
  HttpClientSupplier.of().get()
);
final AuthenticationToken token = tokenSupplier.getToken().get();

// Create a client targeted at the auth endpoint
ApiHttpClient client = ClientBuilder.of()
  .defaultClient(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl())
  .build();

// Build Basic auth header
final String auth = Base64.getEncoder().encodeToString(
  (credentials.getClientId() + ":" + credentials.getClientSecret())
    .getBytes(StandardCharsets.UTF_8));

final ApiHttpHeaders headers = new ApiHttpHeaders()
  .withHeader(ApiHttpHeaders.AUTHORIZATION, format("Basic %s", auth))
  .withHeader(ApiHttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded");

// Call the introspection endpoint
final ApiHttpRequest request = new ApiHttpRequest(
  ApiHttpMethod.POST,
  URI.create(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl() + "/oauth/introspect"),
  headers,
  ("token=" + token.getAccessToken()).getBytes(StandardCharsets.UTF_8));

final ApiHttpResponse<TokenIntrospection> result = client.execute(request)
  .thenApply(r -> client.getSerializerService()
    .convertResponse(r, TokenIntrospection.class))
  .get();

System.out.println("Token active: " + result.getBody().isActive());

Token revocation

Token revocation also requires calling the endpoint directly. Use the same credential and header setup as for introspection:
Call the token revocation endpointjava
// Build the revoke request body
final String revokeBody = "token=" + token.getAccessToken() + "&token_type_hint=access_token";
final ApiHttpRequest revokeRequest = new ApiHttpRequest(
  ApiHttpMethod.POST,
  URI.create(ServiceRegion.GCP_EUROPE_WEST1.getAuthUrl() + "/oauth/token/revoke"),
  headers,
  revokeBody.getBytes(StandardCharsets.UTF_8));

// A successful revocation returns HTTP 200
final ApiHttpResponse<byte[]> revokeToken = client.execute(revokeRequest).get();

Tune the client

Block execution

In some frameworks there is no support for asynchronous execution and so it is necessary to wait for the responses.

The client can wait for responses with the method .executeBlocking().
This method enforces a timeout for resilience and throws an ApiHttpException.
Execute a request synchronouslyjava
ProjectApiRoot apiRoot = createProjectClient();
Project project = apiRoot.get().executeBlocking().getBody();

Configure the underlying HTTP client

The ApiRootBuilder has create methods which allow passing a preconfigured HTTP client.
Configure an HTTP client with a proxyjava
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy", 8080));
VrapHttpClient httpClient = new CtOkHttp4Client(builder -> builder.proxy(proxy)
  );

ProjectApiRoot apiRoot = ApiRootBuilder
  .of(httpClient)
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .withScopes("your-scopes")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .build("my-project");

Custom URLs

To use custom URLs for API endpoints and authentication, provide the base URIs to the defaultClient() method:
Configure custom API and authentication URLsjava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    URI.create("https://auth.custom.example.com/oauth/token"),
    URI.create("https://api.custom.example.com")
  )
  .build("my-project-key");

Not-found middleware

By default, ErrorMiddleware throws an exception for any response with a status code of 400 or higher, including 404 Not Found. To return null instead of throwing a NotFoundException for 404 responses, use addNotFoundExceptionMiddleware():
Return null instead of throwing for 404 responsesjava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .addNotFoundExceptionMiddleware()
  .build("my-project-key");

You can also filter which requests use this behavior by passing a predicate to the method.

Limit concurrent requests

By default the client is initialized with a ForkJoinPool. The underlying HTTP client (OkHttp and Apache AsyncHTTP) is configured for a maximum of 64 concurrent requests by default.

To adjust the maximum concurrent requests, instantiate the HTTP client directly:

Limit concurrent requests with a custom thread pooljava
VrapHttpClient httpClient = new CtOkHttp4Client(
  builder -> builder.dispatcher(
    new Dispatcher(Executors.newFixedThreadPool(20))
  )
);

ProjectApiRoot apiRoot = ApiRootBuilder
  .of(httpClient)
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .build("my-project-key");
Alternatively, use QueueMiddleware to limit concurrent requests at the middleware level:
Limit concurrent requests with QueueMiddlewarejava
ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .addMiddleware(new QueueMiddleware(20))
  .build("my-project-key");

Timeouts

To build resilient applications, it's essential to configure timeouts and retries correctly. The underlying HTTP clients are configured to timeout after 120 seconds by default. You can take several approaches to limiting the time a request can take. For guidance about timeout values, see Error handling through timeout and retries

Middleware-based timeout

The most flexible approach is to use a middleware that times out when needed. This lets you apply different timeouts to different types of requests. The failsafe library is recommended for implementing this:
Configure a middleware-based timeoutjava
Timeout<ApiHttpResponse<byte[]>> timeout = Timeout
  .<ApiHttpResponse<byte[]>>builder(Duration.ofSeconds(10))
  .build();
FailsafeExecutor<ApiHttpResponse<byte[]>> executor = Failsafe.with(timeout);

Middleware timeoutMiddleware = (request, next) ->
  executor.getStageAsync(() -> next.apply(request));

ProjectApiRoot apiRoot = ApiRootBuilder
  .of()
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .addMiddleware(timeoutMiddleware)
  .build("my-project-key");

HTTP client timeout

The underlying HTTP client can be configured with a fixed timeout:

Configure the HTTP client timeoutjava
// OkHttp
VrapHttpClient httpClient = new CtOkHttp4Client(
  builder -> builder
    .connectTimeout(Duration.ofSeconds(5))
    .readTimeout(Duration.ofSeconds(30))
);

// Apache AsyncHTTP
VrapHttpClient httpClient = new CtApacheHttpClient(
  builder -> builder.setResponseTimeout(Timeout.ofSeconds(30))
);

Future timeout

The third option is to use the timeout functionality of Java CompletableFuture:
Set a timeout on a CompletableFuturejava
apiRoot
  .shoppingLists()
  .get()
  .execute()
  .orTimeout(10, TimeUnit.SECONDS)
  .thenAccept(response -> {
    // handle response
  });

HTTP protocol version

OkHttp and Apache AsyncHTTP use HTTP/1.1 by default. To specify a different HTTP protocol version, configure the HTTP client package directly:

Enable HTTP/2java
VrapHttpClient httpClient = new CtOkHttp4Client(
  builder -> builder.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
);

ProjectApiRoot apiRoot = ApiRootBuilder
  .of(httpClient)
  .defaultClient(
    ClientCredentials
      .of()
      .withClientId("your-client-id")
      .withClientSecret("your-client-secret")
      .build(),
    ServiceRegion.GCP_EUROPE_WEST1
  )
  .build("my-project-key");

Subscriptions

Deserializing delivery payloads

The SDK provides JsonUtils which can deserialize subscription delivery payloads. Using the DeliveryPayload class, it correctly deserializes the payload into one of:
Deserialize a subscription delivery payloadjava
String deliveryPayloadJson = "..."; // raw JSON from subscription delivery

DeliveryPayload payload = JsonUtils
  .fromJsonString(deliveryPayloadJson, DeliveryPayload.class);

if (payload instanceof MessageDeliveryPayload) {
  MessageDeliveryPayload messagePayload = (MessageDeliveryPayload) payload;
  // access message-specific fields
}
MessageDeliveryPayload also includes a helper method to cast to the inner message delivery and return the wrapped message:
Access the inner message from a delivery payloadjava
MessageDeliveryPayload messagePayload = (MessageDeliveryPayload) payload;
Message message = messagePayload.getMessage();

Optimize package size

When developing small function applications in size-constrained environments such as AWS Lambda, the full commercetools-sdk-java-api package may be larger than needed.

The SDK offers packages split by domain. You must explicitly declare the domain packages you intend to use.

Artifact IDDescription
commercetools-sdk-java-api-baseBase SDK client infrastructure
commercetools-sdk-java-api-models-baseBase model classes
commercetools-sdk-java-api-models-{domain}Models for a specific domain (for example, products, orders)
commercetools-sdk-java-api-predicatesQuery Predicate builders