Class ProductAttributeDocumentation
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:
isbnasString, International Standard Book Number
The tshirt product type contains the following attributes:
colorasAttributeLocalizedEnumValuewith the colors green and red and their translations in German and EnglishsizeasAttributePlainEnumValuewith S, M and XlaundrySymbolsas set ofAttributeLocalizedEnumValuewith temperature and tumble dryingmatchingProductsas set ofProductReferences, which can point to products that are similar to the current productrrpasMoneycontaining the recommended retail priceavailableSinceasLocalDateTimewhich 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.
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.
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