public final class CategoryDocumentation extends Object
0 top
1 men
3 clothing
7 t-shirts
8 jeans
4 shoes
9 sandals
10 boots
2 women
5 clothing
11 t-shirts
12 jeans
6 shoes
13 sandals
14 boots
To define the category tree in a CSV format we start with the top level categories so that each parent category appears before its child categories:
externalId,externalParentId,name.de,slug.de,name.en,slug.en
0,,top,top,top,top
1,0,Herren,herren,men,men
2,0,Damen,damen,women,women
3,1,Bekleidung,herrenbekleidung,clothing,mens-clothing
4,1,Schuhe,herrenschuhe,shoes,men-shoes
5,2,Bekleidung,damenbekleidung,clothing,womens-clothing
6,2,Schuhe,damenschuhe,shoes,women-shoes
7,3,T-Shirts,herrenbekleidung-tshirts,t-shirts,tshirts-men
8,3,Jeans,herrenbekleidung-jeans,jeans,jeans-men
9,4,Sandalen,herrenschuhe-sandalen,sandals,sandals-men
10,4,Stiefel,herrenschuhe-stiefel,boots,boots-men
11,5,T-Shirts,damenbekleidung-tshirts,t-shirts,tshirts-women
12,5,Jeans,damenbekleidung-jeans,jeans,jeans-women
13,6,Sandalen,damenschuhe-sandalen,sandals,sandals-women
14,6,Stiefel,damenschuhe-stiefel,boots,boots-women
Then we can write some script to parse the csv and create them in sphere:
final Map<String, Category> externalIdToCategoryMap = new HashMap<>();//contains all the created categories try (final InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("category-import-1.csv")) { try (final InputStreamReader inputStreamReader = new InputStreamReader(resourceAsStream)) { try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { final List<Category> categories = bufferedReader.lines() .skip(1)//first line is headers .map(line -> line.trim()) .filter(line -> !"".equals(line))//remove empty lines .map(line -> line.split(",")) .map(columns -> { final LocalizedString name = LocalizedString.of(GERMAN, columns[2]).plus(ENGLISH, columns[4]); final LocalizedString slug = LocalizedString.of(GERMAN, columns[3]).plus(ENGLISH, columns[5]); final String externalId = columns[0]; final CategoryDraftBuilder categoryDraftBuilder = CategoryDraftBuilder.of(name, slug).externalId(externalId).key(randomKey()); final String externalIdParent = columns[1]; if (!"".equals(externalIdParent)) { final Category parent = externalIdToCategoryMap.get(externalIdParent); requireNonNull(parent, externalIdParent + " "); categoryDraftBuilder.parent(parent); } final CategoryDraft draft = categoryDraftBuilder.build(); //here is the call to the Composable Commerce API final Category category = client().executeBlocking(CategoryCreateCommand.of(draft)); externalIdToCategoryMap.put(externalId, category); return category; }) .collect(toList()); return categories; } } }
See the test code.
QueryExecutionUtils
to do this:
final CompletionStage<List<Category>> categoriesStage = QueryExecutionUtils.queryAll(client(), CategoryQuery.of(), 500); final List<Category> categories = SphereClientUtils.blockingWait(categoriesStage, Duration.ofMinutes(1)); assertThat(categories) .hasSize(15) .matches(cats -> cats.parallelStream().anyMatch(cat -> cat.getSlug().get(ENGLISH).equals("boots-women")));
See the test code.
final CategoryQuery seedQuery = CategoryQuery.of().withPredicates(m -> m.parent().isNotPresent()); final CompletionStage<List<Category>> categoriesStage = QueryExecutionUtils.queryAll(client(), seedQuery); final List<Category> rootCategories = SphereClientUtils.blockingWait(categoriesStage, Duration.ofMinutes(1)); assertThat(rootCategories.stream().allMatch(cat -> cat.getParent() == null)) .overridingErrorMessage("fetched only root categories") .isTrue();
See the test code.
//stuff from previous example final CompletionStage<List<Category>> categoriesStage = QueryExecutionUtils.queryAll(client(), CategoryQuery.of()); final List<Category> categories = SphereClientUtils.blockingWait(categoriesStage, Duration.ofMinutes(1)); return CategoryTree.of(categories);
See the test code.
CategoryTree
you can find categories by slug, by ID and search categories by their parent.
final CategoryTree categoryTree = createCategoryTree(); //find by slug final Optional<Category> jeansWomenOptional = categoryTree.findBySlug(ENGLISH, "jeans-women"); assertThat(jeansWomenOptional).isPresent(); assertThat(categoryTree.findBySlug(ENGLISH, "not-existing-slug")).isEmpty(); //find by Key final Optional<Category> jeansWomenOptionalByKey = categoryTree.findByKey(jeansWomenOptional.get().getKey()); assertThat(jeansWomenOptionalByKey).isPresent(); assertThat(jeansWomenOptionalByKey.get().getSlug().get(ENGLISH)).contains("jeans-women"); //find by ID final Reference<Category> clothingWomenReference = jeansWomenOptional.get().getParent(); final Optional<Category> clothingWomenOptional = categoryTree.findById(clothingWomenReference.getId()); assertThat(clothingWomenOptional).isPresent(); assertThat(clothingWomenOptional.get().getSlug().get(ENGLISH)).contains("womens-clothing"); //find all direct children of one category final Category clothingWomen = clothingWomenOptional.get(); final List<Category> clothingWomenSubcategories = categoryTree.findChildren(clothingWomen); final List<String> names = clothingWomenSubcategories.stream() .map(cat -> cat.getName().get(ENGLISH)) .sorted() .collect(toList()); assertThat(names).contains("jeans", "t-shirts");
See the test code.
import io.sphere.sdk.categories.Category;
import io.sphere.sdk.categories.CategoryTree;
import org.apache.commons.lang3.StringUtils;
import java.util.Comparator;
import java.util.List;
import static java.util.Locale.ENGLISH;
import static org.assertj.core.api.Assertions.assertThat;
public final class CategoryTreeTextRepresentation { private static final Comparator<Category> EXTERNALID_COMPARATOR = Comparator.comparing(c -> Integer.parseInt(c.getExternalId())); private CategoryTreeTextRepresentation() { } public static void demoForRendering(final CategoryTree categoryTree) { final String actual = visualizeTree(categoryTree); assertThat(actual).isEqualTo( "0 top\n" + " 1 men\n" + " 3 clothing\n" + " 7 t-shirts\n" + " 8 jeans\n" + " 4 shoes\n" + " 9 sandals\n" + " 10 boots\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n"); } public static String visualizeTree(final CategoryTree categoryTree) { final StringBuilder stringBuilder = new StringBuilder(); categoryTree.getRoots() .forEach(category -> appendToBuilder(category, stringBuilder, categoryTree, 0)); return stringBuilder.toString(); } private static void appendToBuilder(final Category category, final StringBuilder stringBuilder, final CategoryTree categoryTree, final int level) { final String name = category.getName().get(ENGLISH); final String externalId = category.getExternalId(); final String offset = StringUtils.repeat(' ', level * 4); stringBuilder.append(offset).append(externalId).append(" ").append(name).append("\n"); final List<Category> children = categoryTree.findChildren(category); children.stream() .sorted(EXTERNALID_COMPARATOR) .forEach(child -> appendToBuilder(child, stringBuilder, categoryTree, level + 1)); } }
See the test code.
final CategoryTree categoryTree = createCategoryTree(); final Category currentCategory = categoryTree.findBySlug(ENGLISH, "tshirts-men").get(); final List<Reference<Category>> ancestorReferences = currentCategory.getAncestors().stream() .skip(1)//remove top level category .collect(toList()); final Function<Category, String> formatCategory = cat -> defaultString(cat.getExternalId()) + " " + cat.getName().find(ENGLISH).orElse(""); final String ancestorCategoriesString = ancestorReferences.stream() .map(ref -> categoryTree.findById(ref.getId()).get()) .map(formatCategory) .collect(joining(" > ")); final String actual = ancestorCategoriesString + " > " + formatCategory.apply(currentCategory); assertThat(actual).isEqualTo("1 men > 3 clothing > 7 t-shirts");
See the test code.
import io.sphere.sdk.categories.Category;
import io.sphere.sdk.categories.CategoryTree;
import io.sphere.sdk.models.Identifiable;
import io.sphere.sdk.models.Reference;
import org.apache.commons.lang3.StringUtils;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import static java.util.Locale.ENGLISH;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
public final class RenderAPartialTree { private static final Comparator<Category> EXTERNALID_COMPARATOR = Comparator.comparing(c -> Integer.parseInt(c.getExternalId())); private RenderAPartialTree() { } public static void demoForRendering(final CategoryTree categoryTree) { final Category currentCategory = categoryTree.findBySlug(ENGLISH, "tshirts-men").get(); final List<Reference<Category>> ancestorReferences = currentCategory.getAncestors().stream() .skip(1)//remove top level category .collect(toList()); final StringBuilder stringBuilder = new StringBuilder(); appendToBuilder(ancestorReferences.get(0), stringBuilder, categoryTree, 0, currentCategory); final String actual = stringBuilder.toString(); assertThat(actual).isEqualTo( "1 men\n" + " 3 clothing\n" + " ***7 t-shirts***\n" + " 8 jeans\n" + " 4 shoes\n"); } private static void appendToBuilder(final Identifiable<Category> categoryReference, final StringBuilder stringBuilder, final CategoryTree categoryTree, final int level, final Category selectedCategory) { final Category category = categoryTree.findById(categoryReference.getId()).get(); final String name = category.getName().get(ENGLISH); final String externalId = category.getExternalId(); final String offset = StringUtils.repeat(' ', level * 4); stringBuilder.append(offset); if (categoryReference.getId().equals(selectedCategory.getId())) { stringBuilder.append("***"); } stringBuilder.append(externalId).append(" ").append(name); if (categoryReference.getId().equals(selectedCategory.getId())) { stringBuilder.append("***"); } stringBuilder.append("\n"); final Predicate<Category> isAncestor = cat -> selectedCategory.getAncestors().stream().anyMatch(anc -> anc.getId().equals(cat.getId())); final Predicate<Category> isOnHigherLevelThanCurrent = cat -> cat.getAncestors().size() < selectedCategory.getAncestors().size(); final Predicate<Category> isSibling = cat -> cat.getAncestors().equals(selectedCategory.getAncestors()); final List<Category> children = categoryTree.findChildren(category); children.stream() .filter(isAncestor.or(isOnHigherLevelThanCurrent).or(isSibling)) .sorted(EXTERNALID_COMPARATOR) .forEach(child -> appendToBuilder(child, stringBuilder, categoryTree, level + 1, selectedCategory)); } }
See the test code.
final CategoryTree categoryTree = fetchCurrentTree(); final String startingSituation = CategoryTreeTextRepresentation.visualizeTree(categoryTree); assertThat(startingSituation).isEqualTo( "0 top\n" + " 1 men\n" + " 3 clothing\n" + " 7 t-shirts\n" + " 8 jeans\n" + " 4 shoes\n" + " 9 sandals\n" + " 10 boots\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n"); final Category men = categoryTree.findByExternalId("1").get(); client().executeBlocking(CategoryDeleteCommand.of(men)); final CategoryTree categoryTreeAfterDeletion = fetchCurrentTree(); final String actual = CategoryTreeTextRepresentation.visualizeTree(categoryTreeAfterDeletion); assertThat(actual).isEqualTo( "0 top\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n"); //end example parsing here beforeClass();
See the test code.
In the following example we move the mens-clothing category to the top category:
final CategoryTree categoryTree = fetchCurrentTree(); final String startingSituation = CategoryTreeTextRepresentation.visualizeTree(categoryTree); assertThat(startingSituation).isEqualTo( "0 top\n" + " 1 men\n" + " 3 clothing\n" + " 7 t-shirts\n" + " 8 jeans\n" + " 4 shoes\n" + " 9 sandals\n" + " 10 boots\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n"); final Category mensClothing = categoryTree.findByExternalId("3").get(); final Category top = categoryTree.findByExternalId("0").get(); //make mensClothing a child of top client().executeBlocking(CategoryUpdateCommand.of(mensClothing, ChangeParent.of(top))); final CategoryTree categoryTreeAfterMovement = fetchCurrentTree(); final String actual = CategoryTreeTextRepresentation.visualizeTree(categoryTreeAfterMovement); assertThat(actual).isEqualTo( "0 top\n" + " 1 men\n" + " 4 shoes\n" + " 9 sandals\n" + " 10 boots\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n" + " 3 clothing\n" + " 7 t-shirts\n" + " 8 jeans\n"); //end example parsing here beforeClass();
See the test code.
final CategoryTree categoryTree = fetchCurrentTree(); final String startingSituation = CategoryTreeTextRepresentation.visualizeTree(categoryTree); assertThat(startingSituation).isEqualTo( "0 top\n" + " 1 men\n" + " 3 clothing\n" + " 7 t-shirts\n" + " 8 jeans\n" + " 4 shoes\n" + " 9 sandals\n" + " 10 boots\n" + " 2 women\n" + " 5 clothing\n" + " 11 t-shirts\n" + " 12 jeans\n" + " 6 shoes\n" + " 13 sandals\n" + " 14 boots\n"); final Category mensClothingCategory = categoryTree.findByExternalId("3").get(); final Category tshirtCategory = categoryTree.findByExternalId("7").get(); final Category jeansCategory = categoryTree.findByExternalId("8").get(); withProductInCategory(client(), jeansCategory, jeansProduct -> { withProductInCategory(client(), tshirtCategory, tshirtProduct -> { final ProductProjectionQuery sphereRequest = ProductProjectionQuery.ofStaged().withPredicates(m -> m.categories().isIn(asList(mensClothingCategory))); final PagedQueryResult<ProductProjection> resultForParentCategory = //query for the parent category client().executeBlocking(sphereRequest); assertThat(resultForParentCategory.getResults()) .overridingErrorMessage( "if a product is in a category," + "but not in the parent category of this category" + "the query will not find the product") .isEmpty(); final ProductProjectionQuery query = ProductProjectionQuery.ofStaged().withPredicates(m -> m.categories().isIn(asList(tshirtCategory, jeansCategory))); assertThat(query.predicates().get(0)) .isEqualTo(QueryPredicate.of(format("categories(id in (\"%s\", \"%s\"))", tshirtCategory.getId(), jeansCategory.getId()))); final PagedQueryResult<ProductProjection> resultForDirectCategories = client().executeBlocking(query); assertThat(resultForDirectCategories.getResults()) .hasSize(2) .overridingErrorMessage("if a product is in a category, you can directy query for it") .matches(elements -> elements.stream() .anyMatch(product -> product.getCategories().contains(tshirtCategory.toReference()))); }); });
See the test code.
A detailed explanation how to filter and facet for products in categories can be found in ProductCategoriesIdTermFilterSearchModel
and ProductCategoriesIdTermFacetSearchModel
.