public final class ProductAttributeDocumentation extends Object
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
as String
, International Standard Book NumberThe tshirt product type contains the following attributes:
color
as LocalizedEnumValue
with the colors green and red and their translations in German and Englishsize
as EnumValue
with S, M and XlaundrySymbols
as set of LocalizedEnumValue
with temperature and tumble dryingmatchingProducts
as set of product Reference
s, which can point to products that are similar to the current productrrp
as MonetaryAmount
containing the recommended retail priceavailableSince
as LocalDateTime
which contains the date since when the product is available for the customer in the shopAll available attribute types you can find here: AttributeType
in "All Known Implementing Classes".
The code for the creation of the book ProductType
:
final AttributeDefinition isbn = AttributeDefinitionBuilder .of(ISBN_ATTR_NAME, en("ISBN"), StringAttributeType.of()) .build(); final ProductTypeDraft productTypeDraft = ProductTypeDraft.of(randomKey(), BOOK_PRODUCT_TYPE_NAME, "books", asList(isbn)); final ProductType productType = client().executeBlocking(ProductTypeCreateCommand.of(productTypeDraft));
See the test code.
The code for the creation of the tshirt ProductType
:
final LocalizedEnumValue green = LocalizedEnumValue.of("green", LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün")); final LocalizedEnumValue red = LocalizedEnumValue.of("red", LocalizedString.of(ENGLISH, "red").plus(GERMAN, "rot")); final AttributeDefinition color = AttributeDefinitionBuilder .of(COLOR_ATTR_NAME, en("color"), LocalizedEnumAttributeType.of(red, green)) .required(true) .build(); final EnumValue s = EnumValue.of("S", "S"); final EnumValue m = EnumValue.of("M", "M"); final EnumValue x = EnumValue.of("X", "X"); final AttributeDefinition size = AttributeDefinitionBuilder .of(SIZE_ATTR_NAME, en("size"), EnumAttributeType.of(s, m, x)) .required(true) .build(); final LocalizedEnumValue cold = LocalizedEnumValue.of("cold", LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C")); final LocalizedEnumValue hot = LocalizedEnumValue.of("hot", LocalizedString.of(ENGLISH, "Wash at or below 60°C ").plus(GERMAN, "60°C")); final LocalizedEnumValue tumbleDrying = LocalizedEnumValue.of("tumbleDrying", LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen")); final LocalizedEnumValue noTumbleDrying = LocalizedEnumValue.of("noTumbleDrying", LocalizedString.of(ENGLISH, "no tumble drying").plus(GERMAN, "Nicht im Trommeltrockner trocknen")); final SetAttributeType laundryLabelType = SetAttributeType.of(LocalizedEnumAttributeType.of(cold, hot, tumbleDrying, noTumbleDrying)); final AttributeDefinition laundrySymbols = AttributeDefinitionBuilder .of(LAUNDRY_SYMBOLS_ATTR_NAME, en("washing labels"), laundryLabelType) .build(); final AttributeDefinition matchingProducts = AttributeDefinitionBuilder .of(MATCHING_PRODUCTS_ATTR_NAME, en("matching products"), SetAttributeType.of(ReferenceAttributeType.ofProduct())) .build(); final AttributeDefinition rrp = AttributeDefinitionBuilder .of(RRP_ATTR_NAME, en("recommended retail price"), MoneyAttributeType.of()) .build(); final AttributeDefinition availableSince = AttributeDefinitionBuilder .of(AVAILABLE_SINCE_ATTR_NAME, en("available since"), DateAttributeType.of()) .build(); final List<AttributeDefinition> attributes = asList(color, size, laundrySymbols, matchingProducts, rrp, availableSince); final ProductTypeDraft productTypeDraft = ProductTypeDraft.of(randomKey(), PRODUCT_TYPE_NAME, "a 'T' shaped cloth", attributes); final ProductType productType = client().executeBlocking(ProductTypeCreateCommand.of(productTypeDraft));
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.
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 = client().executeBlocking(ProductTypeQuery.of().byName(PRODUCT_TYPE_NAME)).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.plusAttribute(java.lang.String, java.lang.Object)
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 = client().executeBlocking(ProductTypeQuery.of().byName(BOOK_PRODUCT_TYPE_NAME)).head().get(); final ProductVariantDraft masterVariantDraft = ProductVariantDraftBuilder.of() .plusAttribute(ISBN_ATTR_NAME, "978-3-86680-192-9") .build(); final ProductDraft draft = ProductDraftBuilder .of(productType, en("a book"), randomSlug(), masterVariantDraft) .build(); final Product product = client().executeBlocking(ProductCreateCommand.of(draft)); final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant(); assertThat(masterVariant.findAttribute(ISBN_ATTR_NAME, AttributeAccess.ofText())) .contains("978-3-86680-192-9"); return product;
See the test code.
A tshirt example:
final ProductType productType = fetchProductTypeByName(); final Reference<Product> similarProductReference = ProductFixtures.referenceableProduct(client()).toReference(); final ProductVariantDraft masterVariantDraft = ProductVariantDraftBuilder.of() .plusAttribute(COLOR_ATTR_NAME, "green")//special case: any enums are set with key (String) .plusAttribute(SIZE_ATTR_NAME, "S")//special case: any enums are set with key (String) .plusAttribute(LAUNDRY_SYMBOLS_ATTR_NAME, asSet("cold", "tumbleDrying"))//special case: java.util.Set of any enums is set with java.util.Set of keys (String) .plusAttribute(MATCHING_PRODUCTS_ATTR_NAME, asSet(similarProductReference)) .plusAttribute(RRP_ATTR_NAME, MoneyImpl.of(300, EUR)) .plusAttribute(AVAILABLE_SINCE_ATTR_NAME, LocalDate.of(2015, 2, 2)) .build(); final ProductDraft draft = ProductDraftBuilder .of(productType, en("basic shirt"), randomSlug(), masterVariantDraft) .build(); final Product product = client().executeBlocking(ProductCreateCommand.of(draft)); final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant(); assertThat(masterVariant.findAttribute(COLOR_ATTR_NAME, AttributeAccess.ofLocalizedEnumValue())) .overridingErrorMessage("on the get side, the while enum is delivered") .contains(LocalizedEnumValue.of("green", LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün"))); assertThat(masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccess.ofEnumValue())) .contains(EnumValue.of("S", "S")); final LocalizedEnumValue cold = LocalizedEnumValue.of("cold", LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C")); final LocalizedEnumValue tumbleDrying = LocalizedEnumValue.of("tumbleDrying", LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen")); assertThat(masterVariant.findAttribute(LAUNDRY_SYMBOLS_ATTR_NAME, AttributeAccess.ofLocalizedEnumValueSet())) .contains(asSet(cold, tumbleDrying)); assertThat(masterVariant.findAttribute(MATCHING_PRODUCTS_ATTR_NAME, AttributeAccess.ofProductReferenceSet())) .contains(asSet(similarProductReference)); assertThat(masterVariant.findAttribute(RRP_ATTR_NAME, AttributeAccess.ofMoney())) .contains(MoneyImpl.of(300, EUR)); assertThat(masterVariant.findAttribute(AVAILABLE_SINCE_ATTR_NAME, AttributeAccess.ofDate())) .contains(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 = ProductVariantDraftBuilder.of() .plusAttribute(COLOR_ATTR_NAME, 1)//1 is of illegal type and of illegal key .build(); final ProductDraft draft = ProductDraftBuilder .of(productType, en("basic shirt"), randomSlug(), masterVariantDraft) .build(); assertThatThrownBy(() -> client().executeBlocking(ProductCreateCommand.of(draft))) .isInstanceOf(ErrorResponseException.class) .matches(e -> ((ErrorResponseException)e).hasErrorCode(InvalidField.CODE));
See the test code.
/* the declarations you could put into a separate class */ final NamedAttributeAccess<LocalizedEnumValue> color = AttributeAccess.ofLocalizedEnumValue().ofName(COLOR_ATTR_NAME); final NamedAttributeAccess<EnumValue> size = AttributeAccess.ofEnumValue().ofName(SIZE_ATTR_NAME); final NamedAttributeAccess<Set<LocalizedEnumValue>> laundrySymbols = AttributeAccess.ofLocalizedEnumValueSet().ofName(LAUNDRY_SYMBOLS_ATTR_NAME); final NamedAttributeAccess<Set<Reference<Product>>> matchingProducts = AttributeAccess.ofProductReferenceSet().ofName(MATCHING_PRODUCTS_ATTR_NAME); final NamedAttributeAccess<MonetaryAmount> rrp = AttributeAccess.ofMoney().ofName(RRP_ATTR_NAME); final NamedAttributeAccess<LocalDate> availableSince = AttributeAccess.ofDate().ofName(AVAILABLE_SINCE_ATTR_NAME); final LocalizedEnumValue cold = LocalizedEnumValue.of("cold", LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C")); final LocalizedEnumValue tumbleDrying = LocalizedEnumValue.of("tumbleDrying", LocalizedString.of(ENGLISH, "tumble drying").plus(GERMAN, "Trommeltrocknen")); final Reference<Product> productReference = ProductFixtures.referenceableProduct(client()).toReference(); final ProductType productType = fetchProductTypeByName(); final ProductVariantDraft masterVariantDraft = ProductVariantDraftBuilder.of() /* other stuff using the NamedAttributeAccess type-safe */ .plusAttribute(color, LocalizedEnumValue.of("green", LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün")))//will extract the key .plusAttribute(size, EnumValue.of("S", "S")) .plusAttribute(laundrySymbols, asSet(cold, tumbleDrying))//will extract the keys, so you do not need to //remember the special cases, there is also no problem mixing the styles .plusAttribute(matchingProducts, asSet(productReference)) // .plusAttribute(matchingProducts, "foobar") won't compile! .plusAttribute(rrp, MoneyImpl.of(300, EUR)) .plusAttribute(availableSince, LocalDate.of(2015, 2, 2)) .build(); final ProductDraft draft = ProductDraftBuilder .of(productType, en("basic shirt"), randomSlug(), masterVariantDraft) .build(); final Product product = client().executeBlocking(ProductCreateCommand.of(draft)); final ProductVariant masterVariant = product.getMasterData().getStaged().getMasterVariant(); assertThat(masterVariant.findAttribute(color)) .contains(LocalizedEnumValue.of("green", LocalizedString.of(ENGLISH, "green").plus(GERMAN, "grün"))); assertThat(masterVariant.findAttribute(size)).contains(EnumValue.of("S", "S")); assertThat(masterVariant.findAttribute(laundrySymbols)).contains(asSet(cold, tumbleDrying)); assertThat(masterVariant.findAttribute(matchingProducts)).contains(asSet(productReference)); assertThat(masterVariant.findAttribute(rrp)).contains(MoneyImpl.of(300, EUR)); assertThat(masterVariant.findAttribute(availableSince)).contains(LocalDate.of(2015, 2, 2));
See the test code.
The simplest way to get the value of the attribute is to use getAsType
methods of Attribute
, like Attribute.getValueAsEnumValue()
:
final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant(); final String attributeValue = masterVariant.findAttribute(SIZE_ATTR_NAME) .map((Attribute a) -> { final EnumValue enumValue = a.getValueAsEnumValue(); return enumValue.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) .map((Attribute a) -> a.getValueAsBoolean()) .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 AttributeAccess
which keeps the type info to deserialize the attribute.
You can reuse the NamedAttributeAccess
declaration if you want to:
final NamedAttributeAccess<EnumValue> size = AttributeAccess.ofEnumValue().ofName(SIZE_ATTR_NAME); final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant(); final Optional<EnumValue> attributeOption = masterVariant.findAttribute(size); assertThat(attributeOption).contains(EnumValue.of("S", "S"));
See the test code.
Or you can access it on the fly:
final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant(); final Optional<EnumValue> attributeOption = masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccess.ofEnumValue()); assertThat(attributeOption).contains(EnumValue.of("S", "S"));
See the test code.
Or you can access it as JsonNode
, for example if you don't know the type or the SDK does not support it yet:
final ProductVariant masterVariant = createProduct().getMasterData().getStaged().getMasterVariant(); final Attribute attr = masterVariant.getAttribute(SIZE_ATTR_NAME); final JsonNode expectedJsonNode = SphereJsonUtils.toJsonNode(EnumValue.of("S", "S")); assertThat(attr.getValue(AttributeAccess.ofJsonNode())).isEqualTo(expectedJsonNode);
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", AttributeAccess.ofBoolean()); 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, AttributeAccess.ofBoolean())) .isInstanceOf(JsonException.class);
See the test code.
DefaultProductAttributeFormatter
The most convenient way of creating a table of attributes is using a subclass of DefaultProductAttributeFormatter
, since you can rely on defaults and just override the behaviour you want to specify. To initialize the class you need the supported locales of the user viewing the table and the cached ProductType
s of the Project. Remember that you can fetch them all using QueryExecutionUtils
.
One example for a subclass:
import io.sphere.sdk.models.LocalizedEnumValue;
import io.sphere.sdk.models.Reference;
import io.sphere.sdk.products.Product;
import io.sphere.sdk.products.ProductCatalogData;
import io.sphere.sdk.products.ProductData;
import io.sphere.sdk.producttypes.ProductType;
import java.time.LocalDate;
import java.util.*;
import static java.util.stream.Collectors.joining;
public final class ProjectNameProductAttributeFormatter extends DefaultProductAttributeFormatter { public ProjectNameProductAttributeFormatter(final Collection<ProductType> productTypes, final List<Locale> locales) { super(productTypes, locales); } /** * Formats a product reference attribute by using the localized product name instead of the ID. * * @param productReference product * @param attribute attribute containing the product reference * @param productType the product type belonging to the product of the attribute * @return localized name of the product or null */ @Override protected String convertProductReference(final Reference<Product> productReference, final Attribute attribute, final ProductType productType) { return Optional.ofNullable(productReference.getObj()) .map(Product::getMasterData) .map(ProductCatalogData::getStaged) .map(ProductData::getName) .map(name -> name.get(getLocales())) .orElse(null); } /** * Formats a product reference set attribute by using the localized product name instead of the ID. * * @param referenceSet products * @param attribute attribute containing the product reference * @param productType the product type belonging to the product of the attribute * @return translated name of the product or null */ @Override protected String convertProductReferenceSet(final Set<Reference<Product>> referenceSet, final Attribute attribute, final ProductType productType) { return referenceSet.stream() .map(ref -> convertProductReference(ref, attribute, productType)) .collect(joining(", ")); } /** * Formats a {@link LocalizedEnumValue} set product attribute by translating the label and coma separate the results which are sorted in the reverse order. * @param localizedEnumValueSet the attribute value to format * @param attribute the attribute to format * @param productType the product type belonging to the product of the attribute * @return formatted attribute value */ @Override protected String convertLocalizedEnumValueSet(final Set<LocalizedEnumValue> localizedEnumValueSet, final Attribute attribute, final ProductType productType) { return localizedEnumValueSet.stream() .map(value -> convertLocalizedEnumValue(value, attribute, productType)) .sorted(Comparator.reverseOrder()) .collect(joining(", ")); } /** * Format a data attribute by using just {@link Object#toString()}. * @param dateValue the attribute value to format * @param attribute the attribute to format * @param productType the product type belonging to the product of the attribute * @return formatted date */ @Override protected String convertDate(final LocalDate dateValue, final Attribute attribute, final ProductType productType) { return dateValue.toString(); } }
See the test code.
The example in action:
import io.sphere.sdk.models.Reference;
import io.sphere.sdk.products.ProductProjection;
import io.sphere.sdk.products.ProductVariant;
import io.sphere.sdk.producttypes.ProductType;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
public class DefaultProductAttributeFormatterDemo { /** * Creates a formatter for your project based on all fetched product types and a list of locales. * Not reusable for users with different locales. * * @param productTypes all the product types of the project * @param locales the locales which should be used to translate labels and values * @return formatter */ public static DefaultProductAttributeFormatter createFormatter(final Collection<ProductType> productTypes, final List<Locale> locales) { return new ProjectNameProductAttributeFormatter(productTypes, locales); } /** * The source code provides an example how to format a table of product attributes * @param attrNamesToShow a list containing the attribute names (the name is used as key) which clarifies which attributes are allowed to displayed and also give an order to display them * @param productProjection the product that is used to the attributes of its master variant * @param formatter the project specific formatter */ public static void example(final List<String> attrNamesToShow, final ProductProjection productProjection, final DefaultProductAttributeFormatter formatter) { final ProductVariant variant = productProjection.getMasterVariant(); final Reference<ProductType> productType = productProjection.getProductType(); final List<Map.Entry<String, String>> attributes = formatter.createAttributeEntryList(variant, productType, attrNamesToShow); final int valueColumnWidth = getMaxTranslatedValueLength(attributes); final int keyColumnWidth = getMaxTranslatedLabelLength(attributes); final StringBuilder stringBuilder = new StringBuilder("\n"); for (final Map.Entry<String, String> entry : attributes) { stringBuilder.append(String.format("%-" + keyColumnWidth + "s", entry.getKey())) .append(" | ") .append(String.format("%-" + valueColumnWidth + "s", entry.getValue())) .append("\n") .append(org.apache.commons.lang3.StringUtils.repeat('-', keyColumnWidth + valueColumnWidth + 3)) .append("\n"); } final String table = stringBuilder.toString(); final String expected = "\n" + "color | green \n" + "----------------------------------------------------------------\n" + "size | S \n" + "----------------------------------------------------------------\n" + "matching products | referenceable product \n" + "----------------------------------------------------------------\n" + "washing labels | tumble drying, Wash at or below 30°C \n" + "----------------------------------------------------------------\n" + "recommended retail price | EUR300.00 \n" + "----------------------------------------------------------------\n" + "available since | 2015-02-02 \n" + "----------------------------------------------------------------\n"; assertThat(table).isEqualTo(expected); } private static int getMaxTranslatedLabelLength(final List<Map.Entry<String, String>> attributes) { return maxRowLengthFor(attributes, Map.Entry::getKey); } private static int getMaxTranslatedValueLength(final List<Map.Entry<String, String>> attributes) { return maxRowLengthFor(attributes, Map.Entry::getValue); } private static int maxRowLengthFor(final List<Map.Entry<String, String>> attributes, final Function<Map.Entry<String, String>, String> rowStringFunction) { return attributes.stream() .map(rowStringFunction) .filter(value -> value != null) .mapToInt(value -> value.length()) .max() .orElse(0); } }
See the test code.
Setting attribute values is like a a product creation:
Example for books:
final Product product = createBookProduct(); final int masterVariantId = 1; final AttributeDraft attributeDraft = AttributeDraft.of(ISBN_ATTR_NAME, "978-3-86680-192-8"); final SetAttribute updateAction = SetAttribute.of(masterVariantId, attributeDraft); final Product updatedProduct = client().executeBlocking(ProductUpdateCommand.of(product, updateAction)); final ProductVariant masterVariant = updatedProduct.getMasterData().getStaged().getMasterVariant(); assertThat(masterVariant.findAttribute(ISBN_ATTR_NAME, AttributeAccess.ofText())) .contains("978-3-86680-192-8");
See the test code.
Example for tshirts:
final Product product = createProduct(); final int masterVariantId = 1; final Function<AttributeDraft, SetAttribute> draft = attrDraft -> SetAttribute.of(masterVariantId, attrDraft); final List<SetAttribute> updateActions = asList( draft.apply(AttributeDraft.of(COLOR_ATTR_NAME, "red")),//don't forget: enum like => use only keys draft.apply(AttributeDraft.of(SIZE_ATTR_NAME, "M")), draft.apply(AttributeDraft.of(LAUNDRY_SYMBOLS_ATTR_NAME, asSet("cold"))), draft.apply(AttributeDraft.of(RRP_ATTR_NAME, MoneyImpl.of(20, EUR))) ); final Product updatedProduct = client().executeBlocking(ProductUpdateCommand.of(product, updateActions)); final ProductVariant masterVariant = updatedProduct.getMasterData().getStaged().getMasterVariant(); assertThat(masterVariant.findAttribute(COLOR_ATTR_NAME, AttributeAccess.ofLocalizedEnumValue())) .contains(LocalizedEnumValue.of("red", LocalizedString.of(ENGLISH, "red").plus(GERMAN, "rot"))); assertThat(masterVariant.findAttribute(SIZE_ATTR_NAME, AttributeAccess.ofEnumValue())) .contains(EnumValue.of("M", "M")); final LocalizedEnumValue cold = LocalizedEnumValue.of("cold", LocalizedString.of(ENGLISH, "Wash at or below 30°C ").plus(GERMAN, "30°C")); assertThat(masterVariant.findAttribute(LAUNDRY_SYMBOLS_ATTR_NAME, AttributeAccess.ofLocalizedEnumValueSet())) .contains(asSet(cold)); assertThat(masterVariant.findAttribute(RRP_ATTR_NAME, AttributeAccess.ofMoney())) .contains(MoneyImpl.of(20, EUR));
See the test code.
ErrorResponseException
with an error code of "InvalidField".
final Product product = createProduct(); assertThatThrownBy(() -> client().executeBlocking(ProductUpdateCommand.of(product, SetAttribute.of(1, AttributeDraft.of(LAUNDRY_SYMBOLS_ATTR_NAME, "cold"))))) .isInstanceOf(ErrorResponseException.class) .matches(e -> ((ErrorResponseException)e).hasErrorCode(InvalidField.CODE));
See the test code.
For unit tests you can create an Attribute
with a
static factory method such as Attribute.of(String, AttributeAccess, Object)
:
final Product referencedProduct = SphereJsonUtils.readObjectFromResource("product1.json", Product.typeReference()); final Reference<Product> productReference = referencedProduct.toReference(); assertThat(productReference.getObj()) .overridingErrorMessage("product reference is expanded") .isNotNull(); final AttributeAccess<Reference<Product>> access = AttributeAccess.ofProductReference(); final Attribute attribute = Attribute.of("attrname", access, productReference); assertThat(attribute.getValue(access)).isEqualTo(productReference); assertThat(attribute.getValue(access).getObj()).isNotNull();
See the test code.
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 LocalizedEnumValue yellow = LocalizedEnumValue.of("yellow", LocalizedString.of(ENGLISH, "yellow").plus(GERMAN, "gelb")); final ProductVariantImportDraft productVariantImportDraft = ProductVariantImportDraftBuilder.of(product.getId(), 1) .attributes( AttributeImportDraft.of(COLOR_ATTR_NAME, yellow), AttributeImportDraft.of(RRP_ATTR_NAME, EURO_30) ).build(); final LineItemImportDraft lineItemImportDraft = LineItemImportDraftBuilder.of(productVariantImportDraft, 1L, Price.of(EURO_30), en("product name")) .build(); final OrderImportDraft orderImportDraft = OrderImportDraftBuilder .ofLineItems(EURO_20, OrderState.COMPLETE, asList(lineItemImportDraft)) .build(); final Order order = client().executeBlocking(OrderImportCommand.of(orderImportDraft)); final ProductVariant productVariant = order.getLineItems().get(0).getVariant(); final Optional<LocalizedEnumValue> colorAttribute = productVariant.findAttribute(COLOR_ATTR_NAME, AttributeAccess.ofLocalizedEnumValue()); assertThat(colorAttribute).contains(yellow); final Optional<MonetaryAmount> rrpAttribute = productVariant.findAttribute(RRP_ATTR_NAME, AttributeAccess.ofMoney()); assertThat(rrpAttribute).contains(EURO_30); final Set<String> presentAttributes = productVariant.getAttributes().stream() .map(attr -> attr.getName()) .collect(toSet()); assertThat(presentAttributes).containsOnly(COLOR_ATTR_NAME, RRP_ATTR_NAME);
See the test code.
Attribute | AttributeDraft | AttributeImportDraft |
|
---|---|---|---|
purpose | read access | write product, create product | order import |
reference expansion | keeps expanded references | no expansion | no expansion |
enum shape | full enum | enum key (String) | full enum |
value constraints | value in product type | value in product type | free, create on the fly |
This feature is experimental.
An example:
import io.sphere.sdk.models.Referenceable;
import io.sphere.sdk.products.*;
import io.sphere.sdk.products.attributes.*;
import io.sphere.sdk.test.IntegrationTest;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static io.sphere.sdk.products.ProductFixtures.withProduct;
import static io.sphere.sdk.producttypes.ProductTypeFixtures.withProductType;
import static io.sphere.sdk.test.SphereTestUtils.*;
import static io.sphere.sdk.utils.SphereInternalUtils.asSet;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
public class NestedAttributeIntegrationTest extends IntegrationTest { static class NutrientInfo { static NamedAttributeAccess<String> quantityContainedUOM = AttributeAccess.ofString().ofName("quantityContainedUOM"); static NamedAttributeAccess<Double> quantityContained = AttributeAccess.ofDouble().ofName("quantityContained"); static NamedAttributeAccess<String> measurementPrecision = AttributeAccess.ofString().ofName("measurementPrecision"); static NamedAttributeAccess<String> nutrientTypeCode = AttributeAccess.ofString().ofName("nutrientTypeCode"); } static class Nutrient { static NamedAttributeAccess<String> servingSizeUOM = AttributeAccess.ofString().ofName("servingSizeUOM"); static NamedAttributeAccess<Double> servingSize = AttributeAccess.ofDouble().ofName("servingSize"); static NamedAttributeAccess<Set<AttributeContainer>> nutrientInformation = AttributeAccess.ofNestedSet().ofName("nutrientInformation"); } static class Banana { static NamedAttributeAccess<String> bananaColor = AttributeAccess.ofString().ofName("bananaColor"); static NamedAttributeAccess<Set<AttributeContainer>> nutrients = AttributeAccess.ofNestedSet().ofName("nutrients"); } @Test public void itShouldBePossibleToDeeplyNestNestedTypesInsideOfSetTypes() throws Exception { withProductType(client(), () -> createNutrientInformation(), nutrientInformationType -> { withProductType(client(), () -> createNutrient(nutrientInformationType), nutrientType -> { withProductType(client(), () -> createBanana(nutrientType), bananaType -> { final ProductDraftBuilder productDraftBuilder = ProductDraftBuilder .of(bananaType, en("Super Banana!"), en(slugify("super-banana")), createBananaVariant()) .description(en("Cool and refreshing blue banana!")); withProduct(client(), productDraftBuilder, product -> { final AttributeContainer attrs = product.getMasterData().getStaged().getMasterVariant(); assertThat(attrs.findAttribute(Banana.bananaColor)).contains("blue"); assertThat(attrs.findAttribute(Banana.nutrients).map(Set::size)).contains(2); final List<AttributeContainer> nutrients = getSortedNutrients(attrs); assertNutrient(nutrients.get(0), 0.05D, "KM", 1); assertNutrientInfo(getNutrientInfos(nutrients.get(0)).get(0), "FIBTG", "PRECISE", 1.3D, "GR"); assertNutrient(nutrients.get(1), 1.5D, "M", 2); assertNutrientInfo(getNutrientInfos(nutrients.get(1)).get(0), "FAT", "APPROXIMATELY", 0.06D, "KG"); assertNutrientInfo(getNutrientInfos(nutrients.get(1)).get(1), "FIBTG", "APPROXIMATELY", 3.8D, "GR"); }); }); }); }); } private List<AttributeContainer> getSortedNutrients(final AttributeContainer banana) { return banana.findAttribute(Banana.nutrients).get().stream() .sorted((c1, c2) -> c1.findAttribute(Nutrient.servingSize).get().compareTo(c2.findAttribute(Nutrient.servingSize).get())) .collect(toList()); } private List<AttributeContainer> getNutrientInfos(final AttributeContainer nutrient) { return nutrient.findAttribute(Nutrient.nutrientInformation).get().stream() .sorted((c1, c2) -> c1.findAttribute(NutrientInfo.nutrientTypeCode).get().compareTo(c2.findAttribute(NutrientInfo.nutrientTypeCode).get())) .collect(toList()); } private void assertNutrient(final AttributeContainer nutrient, final double servingSize, final String servingSizeUOM, final int nutrientInformationSize) { assertThat(nutrient.findAttribute(Nutrient.servingSize)).contains(servingSize); assertThat(nutrient.findAttribute(Nutrient.servingSizeUOM)).contains(servingSizeUOM); assertThat(nutrient.findAttribute(Nutrient.nutrientInformation).map(Set::size)).contains(nutrientInformationSize); } private void assertNutrientInfo(final AttributeContainer nutrientInfo, final String nutrientTypeCode, final String measurementPrecision, final double quantityContained, final String quantityContainedUOM) { assertThat(nutrientInfo.findAttribute(NutrientInfo.nutrientTypeCode)).contains(nutrientTypeCode); assertThat(nutrientInfo.findAttribute(NutrientInfo.measurementPrecision)).contains(measurementPrecision); assertThat(nutrientInfo.findAttribute(NutrientInfo.quantityContained)).contains(quantityContained); assertThat(nutrientInfo.findAttribute(NutrientInfo.quantityContainedUOM)).contains(quantityContainedUOM); } private ProductVariantDraft createBananaVariant() { return ProductVariantDraftBuilder.of() .attributes(Banana.bananaColor.draftOf("blue"), Banana.nutrients.draftOf(asSet(AttributeContainerDraft.of(asList( Nutrient.servingSize.draftOf(1.5D), Nutrient.servingSizeUOM.draftOf("M"), Nutrient.nutrientInformation.draftOf(asSet(AttributeContainerDraft.of( NutrientInfo.nutrientTypeCode.draftOf("FIBTG"), NutrientInfo.measurementPrecision.draftOf("APPROXIMATELY"), NutrientInfo.quantityContained.draftOf(3.8D), NutrientInfo.quantityContainedUOM.draftOf("GR") ), AttributeContainerDraft.of( NutrientInfo.nutrientTypeCode.draftOf("FAT"), NutrientInfo.measurementPrecision.draftOf("APPROXIMATELY"), NutrientInfo.quantityContained.draftOf(0.06D), NutrientInfo.quantityContainedUOM.draftOf("KG") ))) )), AttributeContainerDraft.of(asList( Nutrient.servingSize.draftOf(0.05D), Nutrient.servingSizeUOM.draftOf("KM"), Nutrient.nutrientInformation.draftOf(asSet(AttributeContainerDraft.of(asList( NutrientInfo.nutrientTypeCode.draftOf("FIBTG"), NutrientInfo.measurementPrecision.draftOf("PRECISE"), NutrientInfo.quantityContained.draftOf(1.3D), NutrientInfo.quantityContainedUOM.draftOf("GR") )))) ))))) .sku(randomKey()) .build(); } private ProductTypeDraft createNutrientInformation() { return ProductTypeDraft.of(randomKey(), "NutrientInformation", "NutrientInformation", asList( AttributeDefinitionBuilder.of(NutrientInfo.quantityContainedUOM.getName(), en(NutrientInfo.quantityContainedUOM.getName()), StringAttributeType.of()).build(), AttributeDefinitionBuilder.of(NutrientInfo.quantityContained.getName(), en(NutrientInfo.quantityContained.getName()), NumberAttributeType.of()).build(), AttributeDefinitionBuilder.of(NutrientInfo.measurementPrecision.getName(), en(NutrientInfo.measurementPrecision.getName()), StringAttributeType.of()).build(), AttributeDefinitionBuilder.of(NutrientInfo.nutrientTypeCode.getName(), en(NutrientInfo.nutrientTypeCode.getName()), StringAttributeType.of()).build())); } private ProductTypeDraft createNutrient(final Referenceable<ProductType> nutrientInformationType) { return ProductTypeDraft.of(randomKey(), "Nutrient", "Nutrient", asList( AttributeDefinitionBuilder.of(Nutrient.servingSizeUOM.getName(), en(Nutrient.servingSizeUOM.getName()), StringAttributeType.of()).build(), AttributeDefinitionBuilder.of(Nutrient.servingSize.getName(), en(Nutrient.servingSize.getName()), NumberAttributeType.of()).build(), AttributeDefinitionBuilder.of(Nutrient.nutrientInformation.getName(), en(Nutrient.nutrientInformation.getName()), SetAttributeType.of(NestedAttributeType.of(nutrientInformationType))).searchable(false).build())); } private ProductTypeDraft createBanana(Referenceable<ProductType> nutrientType) { return ProductTypeDraft.of(randomKey(), "Banana", "Banana", asList( AttributeDefinitionBuilder.of(Banana.bananaColor.getName(), en(Banana.bananaColor.getName()), StringAttributeType.of()).build(), AttributeDefinitionBuilder.of(Banana.nutrients.getName(), en(Banana.nutrients.getName()), SetAttributeType.of(NestedAttributeType.of(nutrientType))).searchable(false).build())); } }
See the test code.
A general explanation can be found in the HTTP API tutorial.