Class ProductAttributeDocumentation
Introduction
ProductType Creation
A ProductType
is like a schema that defines how the product attributes are structured.
ProductType
s contain a list of AttributeDefinition
s 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 ProductType
s book and tshirt.
The book product type contains the following attributes:
isbn
asString
, International Standard Book Number
The tshirt product type contains the following attributes:
color
asAttributeLocalizedEnumValue
with the colors green and red and their translations in German and Englishsize
asAttributePlainEnumValue
with S, M and XlaundrySymbols
as set ofAttributeLocalizedEnumValue
with temperature and tumble dryingmatchingProducts
as set ofProductReference
s, which can point to products that are similar to the current productrrp
asMoney
containing the recommended retail priceavailableSince
asLocalDateTime
which contains the date since when the product is available for the customer in the shop
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.
ProductType
s have a key (String)
which can be used as key to logically identify ProductType
s. 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.
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.
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.
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.
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.
-
Method Summary