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.
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.
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
.