T
- The type of the value of this custom object.public interface CustomObject<T> extends ResourceView<CustomObject<T>,CustomObject<com.fasterxml.jackson.databind.JsonNode>>, Referenceable<CustomObject<com.fasterxml.jackson.databind.JsonNode>>
CustomObject
contains an ID, a version, timestamps
like the other resources in Composable Commerce and in addition getContainer()
,
getKey()
and getValue()
.
Container and key are namespaces like in a key-value store. The value is a JSON structure which can hold custom domain models.
Instead of pure JSON it is encouraged to use Java objects (POJO) with the Jackson JSON mapper.Custom objects can be used with custom Java classes and the Jackson mapper used by the SDK.
The following class contains a constructor with all field types and names a parameter for deserialization and getters for serialization.import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
public class Foo extends Base { /** this custom object stores a long */ private final Long baz; /** this custom object stores also a String*/ private final String bar; @JsonCreator public Foo(final String bar, final Long baz) { this.bar = bar; this.baz = baz; } public String getBar() { return bar; } public long getBaz() { return baz; } }
See the test code.
CustomObjectUpsertCommand
:
final String container = "CustomObjectFixtures"; final String key = randomKey(); final Foo value = FOO_DEFAULT_VALUE; final CustomObjectDraft<Foo> draft = CustomObjectDraft.ofUnversionedUpsert(container, key, value, Foo.class); final CustomObjectUpsertCommand<Foo> createCommand = CustomObjectUpsertCommand.of(draft); final CustomObject<Foo> customObject = client.executeBlocking(createCommand); assertThat(customObject.getContainer()).isEqualTo(container); assertThat(customObject.getKey()).isEqualTo(key); final Foo loadedValue = customObject.getValue(); assertThat(loadedValue).isEqualTo(value); //end example parsing here return customObject;
See the test code.
CustomObjectUpsertCommand
.
For update operations you can use CustomObjectDraft.ofVersionedUpdate(io.sphere.sdk.customobjects.CustomObject, Object, com.fasterxml.jackson.core.type.TypeReference)
to use optimistic concurrency control as in here:
CustomObjectFixtures.withCustomObject(client(), customObject -> { final Foo newValue = new Foo("new value", 72L); final CustomObjectDraft<Foo> draft = CustomObjectDraft.ofVersionedUpdate(customObject, newValue, Foo.class); final CustomObjectUpsertCommand<Foo> command = CustomObjectUpsertCommand.of(draft); final CustomObject<Foo> updatedCustomObject = client().executeBlocking(command); final Foo loadedValue = updatedCustomObject.getValue(); assertThat(loadedValue).isEqualTo(newValue); //end example parsing here client().executeBlocking(command); });
See the test code.
CustomObjectDraft.ofUnversionedUpdate(io.sphere.sdk.customobjects.CustomObject, Object, com.fasterxml.jackson.core.type.TypeReference)
CustomObjectFixtures.withCustomObject(client(), customObject -> { final Foo newValue = new Foo("new value", 72L); final CustomObjectDraft<Foo> draft = CustomObjectDraft.ofUnversionedUpdate(customObject, newValue, Foo.class); final CustomObjectUpsertCommand<Foo> createCommand = CustomObjectUpsertCommand.of(draft); final CustomObject<Foo> updatedCustomObject = client().executeBlocking(createCommand); final Foo loadedValue = updatedCustomObject.getValue(); assertThat(loadedValue).isEqualTo(newValue); });
See the test code.
final ObjectMapper objectMapper = SphereJsonUtils.newObjectMapper();//you should cache this one final ObjectNode objectNode = objectMapper.createObjectNode() .put("bar", " a string") .put("baz", 5L); final String key = "pure-json"; final CustomObjectUpsertCommand<JsonNode> command = CustomObjectUpsertCommand.of(CustomObjectDraft.ofUnversionedUpsert(CONTAINER, key, objectNode)); final CustomObject<JsonNode> customObject = client().executeBlocking(command); assertThat(customObject.getValue().get("bar").asText("default value")).isEqualTo(" a string"); assertThat(customObject.getValue().get("baz").asLong(0)).isEqualTo(5);
See the test code.
withCustomObject(client(), existingCustomObject -> { final String container = existingCustomObject.getContainer(); final String key = existingCustomObject.getKey(); final CustomObjectByKeyGet<JsonNode> fetch = CustomObjectByKeyGet.ofJsonNode(container, key); final CustomObject<JsonNode> customObject = client().executeBlocking(fetch); assertThat(customObject).isNotNull(); final JsonNode value = customObject.getValue(); final String expected = existingCustomObject.getValue().getBar(); final String actual = value.get("bar").asText("it is not present"); assertThat(actual).isEqualTo(expected); });
See the test code.
withCustomObject(client(), existingCustomObject -> { final CustomObjectQuery<JsonNode> clientRequest = CustomObjectQuery.ofJsonNode() .withSort(m -> m.createdAt().sort().desc()); final PagedQueryResult<CustomObject<JsonNode>> result = client().executeBlocking(clientRequest); assertThat(result.getResults().stream().filter(item -> item.hasSameIdAs(existingCustomObject)).count()) .isGreaterThanOrEqualTo(1); final String expected = existingCustomObject.getValue().getBar(); final CustomObject<JsonNode> loadedCustomObject = result.head().get(); final JsonNode jsonNode = loadedCustomObject.getValue(); final String actual = jsonNode.get("bar").asText("it is not present"); assertThat(actual).isEqualTo(expected); });
See the test code.
If you need to create your own models and you prefer another JSON tool over Jackson you can use it at your own risk.
public class GsonFooCustomObjectDraft { private final GsonFoo value; private final String container; private final String key; public GsonFooCustomObjectDraft(final String container, final String key, final GsonFoo value) { this.container = container; this.value = value; this.key = key; } }
See the test code.
import io.sphere.sdk.models.Base;
public class GsonFoo extends Base { private final Long baz; private final String bar; public GsonFoo(final String bar, final Long baz) { this.bar = bar; this.baz = baz; } public String getBar() { return bar; } public long getBaz() { return baz; } }
See the test code.
Extend CustomObjectCustomJsonMappingUpsertCommand
to create or update a custom object with Google Gson:
import com.google.gson.Gson;
import io.sphere.sdk.client.SphereRequestUtils;
import io.sphere.sdk.customobjects.commands.CustomObjectCustomJsonMappingUpsertCommand;
import io.sphere.sdk.http.HttpResponse;
public class GsonFooCustomObjectUpsertCommand extends CustomObjectCustomJsonMappingUpsertCommand<GsonFoo> { private final Gson gson = new Gson(); private final GsonFooCustomObjectDraft draft; public GsonFooCustomObjectUpsertCommand(final GsonFooCustomObjectDraft draft) { this.draft = draft; } //used to send the value of the object @Override protected String bodyAsJsonString() { return gson.toJson(draft); } //used to deserialize the response @Override public GsonFooCustomObject deserialize(final HttpResponse httpResponse) { final String jsonAsString = SphereRequestUtils.getBodyAsString(httpResponse); return gson.fromJson(jsonAsString, GsonFooCustomObject.class); } }
See the test code.
import com.google.gson.Gson;
import io.sphere.sdk.client.SphereRequestUtils;
import io.sphere.sdk.customobjects.CustomObject;
import io.sphere.sdk.customobjects.queries.CustomObjectCustomJsonMappingByKeyGet;
import io.sphere.sdk.http.HttpResponse;
public class GsonFooCustomObjectByKeyGet extends CustomObjectCustomJsonMappingByKeyGet<GsonFoo> { public GsonFooCustomObjectByKeyGet(final String container, final String key) { super(container, key); } @Override protected CustomObject<GsonFoo> deserializeCustomObject(final HttpResponse httpResponse) { final String jsonAsString = SphereRequestUtils.getBodyAsString(httpResponse); return new Gson().fromJson(jsonAsString, GsonFooCustomObject.class); } }
See the test code.
CustomObjectFixtures.withCustomObject(client(), co -> { final String container = co.getContainer(); final String key = co.getKey(); final Get<CustomObject<GsonFoo>> fetch = new GsonFooCustomObjectByKeyGet(container, key); final CustomObject<GsonFoo> customObject = client().executeBlocking(fetch); assertThat(customObject).isNotNull(); assertThat(customObject.toReference()).isEqualTo(co.toReference()); final GsonFoo value = customObject.getValue(); assertThat(value).isEqualTo(new GsonFoo("aString", 5L)); });
See the test code.
In this example we want to create unique readable IDs (successive numbers) for customers since the system returns IDs like 8547b810-9dad-11d1-80b4-44c04fd430c8.
The data model contains the last number and the last Composable Commerce customer ID (just to have more than one field).import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
public class CustomerNumberCounter extends Base { private final Long lastUsedNumber; private final String customerId; @JsonCreator public CustomerNumberCounter(final Long lastUsedNumber, final String customerId) { this.lastUsedNumber = lastUsedNumber; this.customerId = customerId; } public long getLastUsedNumber() { return lastUsedNumber; } public String getCustomerId() { return customerId; } }
See the test code.
Imagine, the customer number should start at 1000, so we do the following request for the initialization of the counter:
final long lastUsedNumber = 999; final CustomerNumberCounter value = new CustomerNumberCounter(lastUsedNumber, "<no name>"); final long initialVersionNumber = 0; final CustomObjectDraft<CustomerNumberCounter> draft = //important: it takes version as parameter CustomObjectDraft.ofVersionedUpsert(CONTAINER, KEY, value, initialVersionNumber, CustomerNumberCounter.class); final Command<CustomObject<CustomerNumberCounter>> initialSettingCommand = CustomObjectUpsertCommand.of(draft); final CustomObject<CustomerNumberCounter> initialCounterLoaded = client().executeBlocking(initialSettingCommand); assertThat(initialCounterLoaded.getValue().getLastUsedNumber()) .overridingErrorMessage("We have created the object") .isEqualTo(lastUsedNumber); assertThat(initialCounterLoaded.getVersion()) .overridingErrorMessage("the command increments the version number by 1") .isEqualTo(initialVersionNumber + 1); try { client().executeBlocking(initialSettingCommand); assertThat(true).overridingErrorMessage("execute the initial command a second time will throw an exception").isFalse(); } catch (final ConcurrentModificationException e) { assertThat(true).overridingErrorMessage("Even in a distributed system the nodes will not override existing values with the initial value.").isTrue(); }
See the test code.
An update works this way then:
final CustomObjectByKeyGet<CustomerNumberCounter> fetch = CustomObjectByKeyGet.of(CONTAINER, KEY, CustomerNumberCounter.class); final CustomObject<CustomerNumberCounter> loadedCustomObject = client().executeBlocking(fetch); final long newCustomerNumber = loadedCustomObject.getValue().getLastUsedNumber() + 1; final CustomerNumberCounter value = new CustomerNumberCounter(newCustomerNumber, "whateverid"); final long version = loadedCustomObject.getVersion(); final CustomObjectDraft<CustomerNumberCounter> draft = CustomObjectDraft.ofVersionedUpsert(CONTAINER, KEY, value, version, CustomerNumberCounter.class); final CustomObjectUpsertCommand<CustomerNumberCounter> updateCommand = CustomObjectUpsertCommand.of(draft); final CustomObject<CustomerNumberCounter> updatedCustomObject = client().executeBlocking(updateCommand); assertThat(updatedCustomObject.getValue().getLastUsedNumber()).isEqualTo(newCustomerNumber); try { client().executeBlocking(updateCommand); assertThat(true).overridingErrorMessage("optimistic concurrency control").isFalse(); } catch (final ConcurrentModificationException e) { //start again at the top assertThat(true).overridingErrorMessage("If other nodes have same version saved, they have to fetch again the object and use the new version number").isTrue(); }
See the test code.
Your data model in the custom objects probably will evolve and you should plan for it.
Optional
to make it obvious to callers that this value can be unset.
Before adding the field (there are already saved custom objects):
import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
public class Xyz extends Base { private final String foo; @JsonCreator public Xyz(final String foo) { this.foo = foo; } public String getFoo() { return foo; } }
See the test code.
After adding the field:
import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
import javax.annotation.Nullable;
public class Xyz extends Base { private final String foo; @Nullable private final String bar; @JsonCreator public Xyz(final String foo, @Nullable final String bar) { this.foo = foo; this.bar = bar; } public String getFoo() { return foo; } @Nullable public String getBar() { return bar; } }
See the test code.
final io.sphere.sdk.customobjects.migrations.version2.Xyz xyz = new io.sphere.sdk.customobjects.migrations.version2.Xyz("foo", "bar"); final String bar = Optional.ofNullable(xyz.getBar()).orElse("default value");
See the test code.
Removing fields can be equivalent to ignoring fields.
Consider you have a class with a field "foo":import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
import javax.annotation.Nullable;
public class Xyz extends Base { private final String foo; @Nullable private final String bar; @JsonCreator public Xyz(final String foo, @Nullable final String bar) { this.foo = foo; this.bar = bar; } public String getFoo() { return foo; } @Nullable public String getBar() { return bar; } }
See the test code.
import com.fasterxml.jackson.annotation.JsonCreator;
import io.sphere.sdk.models.Base;
import javax.annotation.Nullable;
public class Xyz extends Base { @Nullable private final String bar; @JsonCreator public Xyz(@Nullable final String bar) { this.bar = bar; } @Nullable public String getBar() { return bar; } }
See the test code.
import com.fasterxml.jackson.annotation.JsonCreator;
public class Uvw { private final String foo; private final String anotherField; @JsonCreator public Uvw(final String foo, final String anotherField) { this.foo = foo; this.anotherField = anotherField; } public String getFoo() { return foo; } public String getAnotherField() { return anotherField; } }
See the test code.
import com.fasterxml.jackson.annotation.JsonCreator;
public class Uvw { private final Foo foo; private final String anotherField; public Uvw(final Foo foo, final String anotherField) { this.foo = foo; this.anotherField = anotherField; } public Foo getFoo() { return foo; } public String getAnotherField() { return anotherField; } public static class Foo { private final String a; private final String b; @JsonCreator public Foo(final String a, final String b) { this.a = a; this.b = b; } public String getA() { return a; } public String getB() { return b; } } }
See the test code.
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "schemaVersion", defaultImpl = UvwSchemaVersion1.class)//here the first version is default implementation, it does not contain "schemaVersion" @JsonSubTypes({ @JsonSubTypes.Type(value = UvwSchemaVersion2.class, name = "2") }) public interface Uvw { Foo getFoo(); String getAnotherField(); @JsonIgnore static Uvw of(final Foo foo, final String anotherField) { return new UvwSchemaVersion2(foo, anotherField); } }
See the test code.
import com.fasterxml.jackson.annotation.JsonCreator;
public class UvwSchemaVersion2 implements Uvw { private final Foo foo; private final String anotherField; private final String schemaVersion = "2"; @JsonCreator public UvwSchemaVersion2(final Foo foo, final String anotherField) { this.foo = foo; this.anotherField = anotherField; } public Foo getFoo() { return foo; } public String getAnotherField() { return anotherField; } public String getSchemaVersion() { return schemaVersion; } }
See the test code.
import com.fasterxml.jackson.annotation.JsonCreator;
public class UvwSchemaVersion1 extends UvwSchemaVersion2 implements Uvw { //the constructor parameter contains the the fields of the first version //so foo is a String @JsonCreator public UvwSchemaVersion1(final String foo, final String anotherField) { super(toFooObject(foo), anotherField); } private static Foo toFooObject(final String foo) { final String[] strings = foo.split("&", 2); final String a = strings[0]; final String b = strings[1]; return new Foo(a, b); } }
See the test code.
final CustomObjectByKeyGet<Uvw> fetch = CustomObjectByKeyGet.of("container", "key", Uvw.class);
See the test code.
Unlike other query models, CustomObjectQueryModel.of()
takes a type parameter of the result type of the custom object.
Notice that it is necessary to explicitly declare the type when requesting the query model, as shown in the following examples:
final QuerySort<CustomObject<JsonNode>> sort = CustomObjectQueryModel.<CustomObject<JsonNode>>of().createdAt().sort().desc(); final QuerySort<CustomObject<Foo>> fooSort = CustomObjectQueryModel.<CustomObject<Foo>>of().createdAt().sort().desc();
See the test code.
Modifier and Type | Method and Description |
---|---|
String |
getContainer()
The container is part of the custom object namespace to find it
|
String |
getKey()
The key is part of the custom object namespace to find it
|
T |
getValue()
The value stored in the custom object.
|
static String |
referenceTypeId()
A type hint for references which resource type is linked in a reference.
|
default Reference<CustomObject<com.fasterxml.jackson.databind.JsonNode>> |
toReference()
Creates a reference to this resource, the reference may not be filled.
|
static String |
validatedContainer(String container) |
static String |
validatedKey(String key) |
getCreatedAt, getId, getLastModifiedAt, getVersion
hasSameIdAs, toResourceIdentifier
String getContainer()
String getKey()
T getValue()
default Reference<CustomObject<com.fasterxml.jackson.databind.JsonNode>> toReference()
Referenceable
toReference
in interface Referenceable<CustomObject<com.fasterxml.jackson.databind.JsonNode>>
static String referenceTypeId()
Reference.getTypeId()