Class ProductAttributeDocumentation

java.lang.Object
com.commercetools.docs.meta.ProductAttributeDocumentation

public final class ProductAttributeDocumentation extends Object
Table of content

Introduction

ProductType Creation

A ProductType is like a schema that defines how the product attributes are structured.

ProductTypes contain a list of AttributeDefinitions which corresponds to the name and type of each attribute, along with some additional information". Each name/type pair must be unique across a Project, so if you create an attribute "foo" of type String, you cannot create another ProductType where "foo" has another type (e.g. LocalizedString). If you do it anyway you get an error message like:

"The attribute with name 'foo' has a different type on product type 'exampleproducttype'."

In this scenario we provide two ProductTypes book and tshirt.

The book product type contains the following attributes:

  • isbn as String, International Standard Book Number

The tshirt product type contains the following attributes:

All available attribute types you can find here: AttributeType in "All Known Implementing Classes".

The code for the creation of the book ProductType:

final AttributeDefinitionDraft isbn = AttributeDefinitionDraft.builder()
        .type(AttributeTextType.of())
        .name(ISBN_ATTR_NAME)
        .label(LocalizedString.ofEnglish("ISBN"))
        .isRequired(false)
        .build();

final ProductType productType = CommercetoolsTestUtils.getProjectApiRoot()
        .productTypes()
        .create(b -> b.key(CommercetoolsTestUtils.randomKey())
                .name(BOOK_PRODUCT_TYPE_NAME)
                .description("books")
                .plusAttributes(isbn))
        .executeBlocking()
        .getBody();

See the test code.

The code for the creation of the tshirt ProductType:

final AttributeLocalizedEnumValue green = AttributeLocalizedEnumValue.builder()
        .key("green")
        .label(LocalizedString.ofEnglish("green").plus(GERMAN, "grün"))
        .build();
final AttributeLocalizedEnumValue red = AttributeLocalizedEnumValue.builder()
        .key("red")
        .label(LocalizedString.of(ENGLISH, "red").plus(GERMAN, "rot"))
        .build();
final AttributeDefinitionDraft color = AttributeDefinitionDraft.builder()
        .name(COLOR_ATTR_NAME)
        .label(LocalizedString.ofEnglish("color"))
        .type(AttributeLocalizedEnumType.builder().plusValues(red, green).build())
        .isRequired(true)
        .build();

final AttributePlainEnumValue s = AttributePlainEnumValue.builder().key("S").label("S").build();
final AttributePlainEnumValue m = AttributePlainEnumValue.builder().key("M").label("M").build();
final AttributePlainEnumValue x = AttributePlainEnumValue.builder().key("X").label("X").build();
final AttributeDefinitionDraft size = AttributeDefinitionDraft.builder()
        .name(SIZE_ATTR_NAME)
        .label(LocalizedString.ofEnglish("size"))
        .type(AttributeEnumType.builder().plusValues(s, m, x).build())
        .isRequired(true)
        .build();

final AttributeLocalizedEnumValue cold = AttributeLocalizedEnumValue.builder()
        .key("cold")
        .label(LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C"))
        .build();
final AttributeLocalizedEnumValue hot = AttributeLocalizedEnumValue.builder()
        .key("hot")
        .label(LocalizedString.of(ENGLISH, "Wash at or below 60°C ").plus(GERMAN, "60°C"))
        .build();
final AttributeLocalizedEnumValue tumbleDrying = AttributeLocalizedEnumValue.builder()
        .key("tumbleDrying")
        .label(LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen"))
        .build();
final AttributeLocalizedEnumValue noTumbleDrying = AttributeLocalizedEnumValue.builder()
        .key("noTumbleDrying")
        .label(
            LocalizedString.of(ENGLISH, "no tumble drying").plus(GERMAN, "Nicht im Trommeltrockner trocknen"))
        .build();
final AttributeSetType laundryLabelType = AttributeSetType.builder()
        .elementType(
            AttributeLocalizedEnumType.builder().plusValues(cold, hot, tumbleDrying, noTumbleDrying).build())
        .build();
final AttributeDefinitionDraft laundrySymbols = AttributeDefinitionDraft.builder()
        .type(laundryLabelType)
        .name(LAUNDRY_SYMBOLS_ATTR_NAME)
        .label(LocalizedString.ofEnglish("washing labels"))
        .isRequired(false)
        .build();

final AttributeDefinitionDraft matchingProducts = AttributeDefinitionDraft.builder()
        .name(MATCHING_PRODUCTS_ATTR_NAME)
        .label(LocalizedString.ofEnglish("matching products"))
        .type(AttributeSetType.builder()
                .elementType(
                    AttributeReferenceType.builder().referenceTypeId(AttributeReferenceTypeId.PRODUCT).build())
                .build())
        .isRequired(false)
        .build();

final AttributeDefinitionDraft rrp = AttributeDefinitionDraft.builder()
        .name(RRP_ATTR_NAME)
        .label(LocalizedString.ofEnglish("recommended retail price"))
        .type(AttributeMoneyType.of())
        .isRequired(false)
        .build();

final AttributeDefinitionDraft availableSince = AttributeDefinitionDraft.builder()
        .name(AVAILABLE_SINCE_ATTR_NAME)
        .label(LocalizedString.ofEnglish("available since"))
        .type(AttributeDateType.of())
        .isRequired(false)
        .build();

final List<AttributeDefinitionDraft> attributes = asList(color, size, laundrySymbols, matchingProducts, rrp,
    availableSince);
final ProductTypeDraft productTypeDraft = ProductTypeDraft.builder()
        .key(CommercetoolsTestUtils.randomKey())
        .name(PRODUCT_TYPE_NAME)
        .description("a 'T' shaped cloth")
        .attributes(attributes)
        .build();
final ProductType productType = CommercetoolsTestUtils.getProjectApiRoot()
        .productTypes()
        .post(productTypeDraft)
        .executeBlocking()
        .getBody();

See the test code.

ProductTypes have a key (String) which can be used as key to logically identify ProductTypes. The key has an unique constraint.

Product Creation

To create a product you need to reference the product type. Since the ProductType ID of the development system will not be the ID of the production system it is necessary to find the product type by name:

final Optional<ProductType> productTypeOptional = CommercetoolsTestUtils.getProjectApiRoot()
        .productTypes()
        .get()
        .addQuery(p -> p.name().is(PRODUCT_TYPE_NAME))
        .executeBlocking()
        .getBody()
        .head();
final ProductType productType = productTypeOptional
        .orElseThrow(() -> new RuntimeException("product type " + PRODUCT_TYPE_NAME + " is not present."));
//end example parsing here
return productType;

See the test code.

The simplest way of adding attributes to a ProductVariant is to use ProductVariantDraftBuilder.plusAttributes(Attribute...) which enables you to directly put the value of the attribute to the draft. But it cannot check if you put the right objects and types in it.

A book example:

final ProductType productType = CommercetoolsTestUtils.getProjectApiRoot()
        .productTypes()
        .get()
        .withQuery(query -> query.name().is(BOOK_PRODUCT_TYPE_NAME))
        .executeBlocking()
        .getBody()
        .head()
        .get();
final ProductVariantDraft masterVariantDraft = ProductVariantDraft.builder()
        .plusAttributes(b -> b.name(ISBN_ATTR_NAME).value("978-3-86680-192-9"))
        .build();
final ProductDraft draft = ProductDraft.builder()
        .productType(productType.toResourceIdentifier())
        .name(LocalizedString.ofEnglish("a book"))
        .slug(randomSlug())
        .masterVariant(masterVariantDraft)
        .build();

final Product product = CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .create(draft)
        .executeBlocking()
        .getBody();

final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant();
assertThat(masterVariant.findAttribute(ISBN_ATTR_NAME).get().getValue()).isEqualTo("978-3-86680-192-9");
return product;

See the test code.

A tshirt example:

final ProductType productType = fetchProductTypeByName();
final ProductReference similarProductReference = ProductFixtures
        .referenceableProduct(CommercetoolsTestUtils.getProjectApiRoot())
        .toReference();
final ProductVariantDraft masterVariantDraft = ProductVariantDraft.builder()
        .plusAttributes(b -> b.name(COLOR_ATTR_NAME).value("green")) //special case: any enums are set with key (String)
        .plusAttributes(b -> b.name(SIZE_ATTR_NAME).value("S"))//special case: any enums are set with key (String)
        .plusAttributes(b -> b.name(LAUNDRY_SYMBOLS_ATTR_NAME).value(asSet("cold", "tumbleDrying")))//special case: java.util.Set of any enums is set with java.util.Set of keys (String)
        .plusAttributes(b -> b.name(MATCHING_PRODUCTS_ATTR_NAME).value(asSet(similarProductReference)))
        .plusAttributes(b -> b.name(RRP_ATTR_NAME).value(Money.builder().centAmount(300L).currencyCode("EUR")))
        .plusAttributes(b -> b.name(AVAILABLE_SINCE_ATTR_NAME).value(LocalDate.of(2015, 2, 2)))
        .build();
final ProductDraft draft = ProductDraft.builder()
        .productType(productType.toResourceIdentifier())
        .name(LocalizedString.ofEnglish("basic shirt"))
        .slug(randomSlug())
        .masterVariant(masterVariantDraft)
        .build();

final Product product = CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .create(draft)
        .executeBlocking()
        .getBody();

final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant();
assertThat(masterVariant.findAttribute(COLOR_ATTR_NAME).get().getValue())
        .overridingErrorMessage("on the get side, the while enum is delivered")
        .isEqualTo(AttributeLocalizedEnumValue.builder()
                .key("green")
                .label(LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün"))
                .build());
assertThat(masterVariant.findAttribute(SIZE_ATTR_NAME).get().getValue())
        .isEqualTo(AttributePlainEnumValue.builder().key("S").label("S").build());
final AttributeLocalizedEnumValue cold = AttributeLocalizedEnumValue.builder()
        .key("cold")
        .label(LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C"))
        .build();
final AttributeLocalizedEnumValue tumbleDrying = AttributeLocalizedEnumValue.builder()
        .key("tumbleDrying")
        .label(LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen"))
        .build();
assertThat(masterVariant.findAttribute(LAUNDRY_SYMBOLS_ATTR_NAME).get().getValue())
        .isEqualTo(asList(cold, tumbleDrying));
assertThat(masterVariant.findAttribute(MATCHING_PRODUCTS_ATTR_NAME).get().getValue())
        .isEqualTo(asList(similarProductReference));
assertThat(masterVariant.findAttribute(RRP_ATTR_NAME).get().getValue())
        .isEqualTo(CentPrecisionMoney.builder().currencyCode("EUR").centAmount(300L).fractionDigits(2).build());
assertThat(masterVariant.findAttribute(AVAILABLE_SINCE_ATTR_NAME).get().getValue())
        .isEqualTo(LocalDate.of(2015, 2, 2));

return product;

See the test code.

A wrong value for a field or an invalid type will cause an ErrorResponseException with an error code of "InvalidField".
final ProductType productType = fetchProductTypeByName();
final ProductVariantDraft masterVariantDraft = ProductVariantDraft.builder()
        .plusAttributes(b -> b.name(COLOR_ATTR_NAME).value(1)) //1 is of illegal type and of illegal key
        .build();
final ProductDraft draft = ProductDraft.builder()
        .productType(productType.toResourceIdentifier())
        .name(LocalizedString.ofEnglish("basic shirt"))
        .slug(randomSlug())
        .masterVariant(masterVariantDraft)
        .build();
assertThatThrownBy(() -> CommercetoolsTestUtils.getProjectApiRoot().products().create(draft).executeBlocking())
        .isInstanceOf(ErrorResponseException.class)
        .matches(e -> ((ErrorResponseException) e).hasErrorCode(InvalidFieldError.INVALID_FIELD));

See the test code.

As alternative you could declare your attributes at the same place and use these to read and write attribute values:
final AttributeLocalizedEnumValue cold = AttributeLocalizedEnumValue.builder()
        .key("cold")
        .label(LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C"))
        .build();
final AttributeLocalizedEnumValue tumbleDrying = AttributeLocalizedEnumValue.builder()
        .key("tumbleDrying")
        .label(LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen"))
        .build();
final ProductReference productReference = ProductFixtures
        .referenceableProduct(CommercetoolsTestUtils.getProjectApiRoot())
        .toReference();
final ProductType productType = fetchProductTypeByName();
final ProductVariantDraft masterVariantDraft = ProductVariantDraft.builder()
        .plusAttributes(builder -> builder.name(COLOR_ATTR_NAME).value("green"))
        .plusAttributes(builder -> builder.name(SIZE_ATTR_NAME)
                .value(AttributePlainEnumValue.builder().key("S").label("S").build()))
        .plusAttributes(builder -> builder.name(LAUNDRY_SYMBOLS_ATTR_NAME).value(asSet("cold", "tumbleDrying")))
        .plusAttributes(builder -> builder.name(MATCHING_PRODUCTS_ATTR_NAME).value(asSet(productReference)))
        .plusAttributes(builder -> builder.name(RRP_ATTR_NAME)
                .value(Money.builder().centAmount(300L).currencyCode("EUR").build()))
        .plusAttributes(builder -> builder.name(AVAILABLE_SINCE_ATTR_NAME).value(LocalDate.of(2015, 2, 2)))
        .build();
final ProductDraft draft = ProductDraft.builder()
        .productType(productType.toResourceIdentifier())
        .name(LocalizedString.ofEnglish("basic shirt"))
        .slug(randomSlug())
        .masterVariant(masterVariantDraft)
        .build();

final Product product = CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .create(draft)
        .executeBlocking()
        .getBody();

final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant();
assertThat(masterVariant.getAttributeByName(COLOR_ATTR_NAME).asLocalizedEnum())
        .isEqualTo(AttributeLocalizedEnumValue.builder()
                .key("green")
                .label(LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün"))
                .build());
assertThat(masterVariant.getAttributeByName(SIZE_ATTR_NAME).asEnum())
        .isEqualTo(AttributePlainEnumValue.builder().key("S").label("S").build());
assertThat(masterVariant.getAttributeByName(LAUNDRY_SYMBOLS_ATTR_NAME).asSetLocalizedEnum())
        .isEqualTo(asList(cold, tumbleDrying));
assertThat(masterVariant.getAttributeByName(MATCHING_PRODUCTS_ATTR_NAME).asSetReference())
        .isEqualTo(singletonList(productReference));
assertThat(masterVariant.getAttributeByName(RRP_ATTR_NAME).asMoney())
        .isEqualTo(CentPrecisionMoney.builder().centAmount(300L).currencyCode("EUR").fractionDigits(2).build());
assertThat(masterVariant.getAttributeByName(AVAILABLE_SINCE_ATTR_NAME).asDate())
        .isEqualTo(LocalDate.of(2015, 2, 2));

See the test code.

Reading Attributes

The simplest way to get the value of the attribute is to use getValue() methods of Attribute, like Attribute.getValue():

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

final String attributeValue = masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccessor::asEnum)
        .map(AttributePlainEnumValue::getLabel)
        .orElse("not found");
assertThat(attributeValue).isEqualTo("S");

See the test code.

If you use a wrong conversion for the attribute, like you have a EnumValue but extract it as boolean then you get a JsonException:

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

final Throwable throwable = catchThrowable(
    () -> masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccessor::asBoolean).orElse(true));
assertThat(throwable).isInstanceOf(JsonException.class);

See the test code.

An alternative way to get a value out of an attribute is to use an instance of AttributesAccessor which keeps the type info to deserialize the attribute.

You can reuse the NamedAttributeAccess declaration if you want to:

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

final Optional<AttributePlainEnumValue> attributeOption = masterVariant.findAttribute(SIZE_ATTR_NAME,
    AttributeAccessor::asEnum);
assertThat(attributeOption).contains(AttributePlainEnumValue.builder().key("S").label("S").build());

See the test code.

Or you can access it on the fly:

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

final Optional<AttributePlainEnumValue> attributeOption = masterVariant.findAttribute(SIZE_ATTR_NAME,
    AttributeAccessor::asEnum);
assertThat(attributeOption).contains(AttributePlainEnumValue.builder().key("S").label("S").build());

See the test code.

If the attribute is not present in the AttributeContainer then the Optional will be empty:

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

final Optional<Boolean> attributeOption = masterVariant.findAttribute("notpresent",
    AttributeAccessor::asBoolean);
assertThat(attributeOption).isEmpty();

See the test code.

If you provide a wrong type, the code will throw a JsonException:

final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant();

assertThatThrownBy(() -> masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccessor::asBoolean))
        .isInstanceOf(JsonException.class);

See the test code.

Update attribute values of a product

Setting attribute values is like a a product creation:

Example for books:

final Product product = createBookProduct();
final long masterVariantId = 1;

final Product updatedProduct = CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .update(product)
        .with(builder -> builder.plus(actionBuilder -> actionBuilder.setAttributeBuilder()
                .variantId(masterVariantId)
                .name(ISBN_ATTR_NAME)
                .value("978-3-86680-192-8")))
        .executeBlocking()
        .getBody();

final ProductVariant masterVariant = updatedProduct.getMasterData().getStaged().getMasterVariant();
assertThat(masterVariant.findAttribute(ISBN_ATTR_NAME).get().getValue()).isEqualTo("978-3-86680-192-8");

See the test code.

Example for tshirts:

final Product product = createProduct();
final long masterVariantId = 1;
final Product updatedProduct = CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .update(product)
        .with(update -> update
                .plus(builder -> builder.setAttributeBuilder()
                        .variantId(masterVariantId)
                        .name(COLOR_ATTR_NAME)
                        .value("red"))
                .plus(builder -> builder.setAttributeBuilder()
                        .variantId(masterVariantId)
                        .name(SIZE_ATTR_NAME)
                        .value("M"))
                .plus(builder -> builder.setAttributeBuilder()
                        .variantId(masterVariantId)
                        .name(LAUNDRY_SYMBOLS_ATTR_NAME)
                        .value(asSet("cold")))
                .plus(builder -> builder.setAttributeBuilder()
                        .variantId(masterVariantId)
                        .name(RRP_ATTR_NAME)
                        .value(Money.builder().currencyCode("EUR").centAmount(2000L))))
        .executeBlocking()
        .getBody();

final ProductVariant masterVariant = updatedProduct.getMasterData().getStaged().getMasterVariant();
assertThat(masterVariant.findAttribute(COLOR_ATTR_NAME, AttributeAccessor::asLocalizedEnum))
        .contains(AttributeLocalizedEnumValue.builder()
                .key("red")
                .label(LocalizedString.of(ENGLISH, "red").plus(GERMAN, "rot"))
                .build());
assertThat(masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccessor::asEnum))
        .contains(AttributePlainEnumValue.builder().key("M").label("M").build());
final AttributeLocalizedEnumValue cold = AttributeLocalizedEnumValue.builder()
        .key("cold")
        .label(LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C"))
        .build();
assertThat(masterVariant.findAttribute(LAUNDRY_SYMBOLS_ATTR_NAME, AttributeAccessor::asSetLocalizedEnum))
        .contains(singletonList(cold));
assertThat(masterVariant.findAttribute(RRP_ATTR_NAME, AttributeAccessor::asMoney))
        .contains(CentPrecisionMoney.builder().centAmount(2000L).currencyCode("EUR").fractionDigits(2).build());

See the test code.

A wrong value for a field or an invalid type (like flat value instead of set) will cause an ErrorResponseException with an error code of "InvalidField".
final Product product = createProduct();
assertThatThrownBy(() -> CommercetoolsTestUtils.getProjectApiRoot()
        .products()
        .update(product)
        .with(update -> update.plus(builder -> builder.setAttributeBuilder()
                .variantId(1L)
                .name(LAUNDRY_SYMBOLS_ATTR_NAME)
                .value("cold")))
        .executeBlocking()).isInstanceOf(ErrorResponseException.class)
        .matches(e -> ((ErrorResponseException) e).hasErrorCode(InvalidFieldError.INVALID_FIELD));

See the test code.

Create attributes for importing orders

Importing attribute values for orders works different from updating products. In orders you provide the full value for enum-like types instead of just the key as done for all other types. This makes it possible to create a new enum value on the fly. The other attributes behave as expected.

Example:

final Product product = createProduct();
//yellow is not defined in the product type, but for order imports this works to add use it on the fly
final AttributeLocalizedEnumValue yellow = AttributeLocalizedEnumValue.builder()
        .key("yellow")
        .label(LocalizedString.of(ENGLISH, "yellow").plus(GERMAN, "gelb"))
        .build();
final ProductVariantImportDraft productVariantImportDraft = ProductVariantImportDraft.builder()
        .id(1L)
        .plusAttributes(builder -> builder.name(COLOR_ATTR_NAME).value(yellow))
        .plusAttributes(builder -> builder.name(RRP_ATTR_NAME).value(TestUtils.EURO_30))
        .build();
final LineItemImportDraft lineItemImportDraft = LineItemImportDraft.builder()
        .productId(product.getId())
        .variant(productVariantImportDraft)
        .quantity(1L)
        .price(p -> p.value(TestUtils.EURO_20))
        .name(LocalizedString.ofEnglish("product name"))
        .build();
final OrderImportDraft orderImportDraft = OrderImportDraft.builder()
        .lineItems(lineItemImportDraft)
        .totalPrice(TestUtils.EURO_20)
        .orderState(OrderState.COMPLETE)
        .build();

final Order order = CommercetoolsTestUtils.getProjectApiRoot()
        .orders()
        .importOrder()
        .post(orderImportDraft)
        .executeBlocking()
        .getBody();

final ProductVariant productVariant = order.getLineItems().get(0).getVariant();
final Optional<AttributeLocalizedEnumValue> colorAttribute = productVariant.findAttribute(COLOR_ATTR_NAME,
    AttributeAccessor::asLocalizedEnum);
assertThat(colorAttribute).contains(yellow);
final Optional<Money> rrpAttribute = productVariant.findAttribute(RRP_ATTR_NAME, AttributeAccessor::asMoney);
assertThat(rrpAttribute).contains(TestUtils.EURO_30);
final Set<String> presentAttributes = productVariant.getAttributes()
        .stream()
        .map(Attribute::getName)
        .collect(toSet());
assertThat(presentAttributes).containsOnly(COLOR_ATTR_NAME, RRP_ATTR_NAME);

See the test code.