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.io.vrap.rmf.base.client.utils.json.ModuleSupplier:
com.commercetools.api.json.ApiModuleSupplier
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).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:ApiModuleOptions options = ApiModuleOptions
.of()
.withAttributeNumberAsDouble(true)
.withCustomFieldNumberAsDouble(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);
You can also set these options using system properties:
commercetools.deserializeAttributeNumberAsDoublecommercetools.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:
ApiModuleOptions options = ApiModuleOptions
.of()
.withAttributeAsJsonNode(true)
.withCustomFieldAsJsonNode(true);
ObjectMapper mapper = JsonUtils.createObjectMapper(options);
You can also set these options using system properties:
commercetools.deserializeAttributeAsJsonNodecommercetools.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: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.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():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: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: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:
// 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.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.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: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():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:
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:
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:
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:
// 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:
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:
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:
- MessageDeliveryPayload
- ResourceCreatedDeliveryPayload
- ResourceUpdatedDeliveryPayload
- ResourceDeletedDeliveryPayload
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: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 ID | Description |
|---|---|
commercetools-sdk-java-api-base | Base SDK client infrastructure |
commercetools-sdk-java-api-models-base | Base model classes |
commercetools-sdk-java-api-models-{domain} | Models for a specific domain (for example, products, orders) |
commercetools-sdk-java-api-predicates | Query Predicate builders |
An example using split packages is available on GitHub.