package productcatalog.controllers;
import common.cms.CmsPage;
import common.contexts.UserContext;
import common.controllers.ControllerDependency;
import common.controllers.SunriseController;
import common.pages.BreadcrumbDataFactory;
import common.pages.SelectableLinkData;
import io.sphere.sdk.categories.Category;
import io.sphere.sdk.categories.CategoryTree;
import io.sphere.sdk.facets.*;
import io.sphere.sdk.models.Reference;
import io.sphere.sdk.products.ProductProjection;
import io.sphere.sdk.products.search.ProductProjectionSearch;
import io.sphere.sdk.products.search.ProductProjectionSearchModel;
import io.sphere.sdk.search.*;
import play.Configuration;
import play.Logger;
import play.i18n.Messages;
import play.libs.F;
import play.mvc.Result;
import productcatalog.models.SortOption;
import productcatalog.models.SortOptionImpl;
import productcatalog.pages.*;
import productcatalog.services.CategoryService;
import productcatalog.services.ProductProjectionService;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import static io.sphere.sdk.facets.DefaultFacetType.HIERARCHICAL_SELECT;
import static io.sphere.sdk.facets.DefaultFacetType.SORTED_SELECT;
import static io.sphere.sdk.search.SimpleSearchSortDirection.ASC;
import static io.sphere.sdk.search.SimpleSearchSortDirection.DESC;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
@Singleton
public class ProductOverviewPageController extends SunriseController {
private static final StringSearchModel<ProductProjection, ?> BRAND_SEARCH_MODEL = ProductProjectionSearchModel.of().allVariants().attribute().ofEnum("designer").label();
private static final StringSearchModel<ProductProjection, ?> COLOR_SEARCH_MODEL = ProductProjectionSearchModel.of().allVariants().attribute().ofLocalizableEnum("color").key();
private static final StringSearchModel<ProductProjection, ?> SIZE_SEARCH_MODEL = ProductProjectionSearchModel.of().allVariants().attribute().ofEnum("commonSize").label();
private static final StringSearchModel<ProductProjection, ?> CATEGORY_SEARCH_MODEL = new StringSearchModel<>(null, "variants.categories.id");
private static final SearchSort<ProductProjection> MODIFIED_SORT_BY_DESC = ProductProjectionSearchModel.of().lastModifiedAt().sorted(DESC);
private static final SearchSort<ProductProjection> PRICE_SORT_BY_DESC = ProductProjectionSearchModel.of().allVariants().price().centAmount().sorted(DESC);
private static final SearchSort<ProductProjection> PRICE_SORT_BY_ASC = ProductProjectionSearchModel.of().allVariants().price().centAmount().sorted(ASC);
private static final String FACET_COLOR_KEY = "color";
private static final String FACET_SIZE_KEY = "size";
private static final String FACET_CATEGORY_KEY = "productType";
private static final String FACET_BRAND_KEY = "brands";
private static final String SORT_NEW_KEY = "new";
private static final String SORT_PRICE_ASC_KEY = "price-asc";
private static final String SORT_PRICE_DESC_KEY = "price-desc";
private final int pageSize;
private final int displayedPages;
private final ProductProjectionService productService;
private final CategoryService categoryService;
@Inject
public ProductOverviewPageController(final Configuration configuration, final ControllerDependency controllerDependency,
final ProductProjectionService productService, final CategoryService categoryService) {
super(controllerDependency);
this.productService = productService;
this.categoryService = categoryService;
this.pageSize = configuration.getInt("pop.pageSize");
this.displayedPages = configuration.getInt("pop.displayedPages");
}
public F.Promise<Result> show(final String locale, final String categorySlug, final int page) {
final UserContext userContext = userContext(locale);
final Messages messages = messages(userContext);
final Optional<Category> category = categories().findBySlug(userContext.locale(), categorySlug);
if (category.isPresent()) {
final List<Category> childrenCategories = categories().findChildren(category.get());
final List<Facet<ProductProjection>> boundFacets = boundFacetList(userContext.locale(), childrenCategories, messages);
final List<SortOption<ProductProjection>> boundSortOptions = boundSortOptionList(messages);
final F.Promise<PagedSearchResult<ProductProjection>> searchResultPromise =
searchProducts(getProductProjectionSearch(category.get()), boundFacets, boundSortOptions, page);
final F.Promise<CmsPage> cmsPromise = cmsService().getPage(userContext.locale(), "pop");
return searchResultPromise.flatMap(searchResult ->
cmsPromise.map(cms -> {
final ProductOverviewPageContent content = getPopPageData(cms, userContext, messages, searchResult, boundFacets, page, category.get().toReference());
return ok(templateService().renderToHtml("pop", pageData(userContext, content)));
})
);
}
return F.Promise.pure(notFound("Category not found: " + categorySlug));
}
public F.Promise<Result> search(final String languageTag, final String searchTerm, final int page) {
final UserContext userContext = userContext(languageTag);
final Messages messages = messages(userContext);
final List<Facet<ProductProjection>> boundFacets = boundFacetList(userContext.locale(), categories().getRoots(), messages);
final List<SortOption<ProductProjection>> boundSortOptions = boundSortOptionList(messages);
final F.Promise<PagedSearchResult<ProductProjection>> searchResultPromise =
searchProducts(getProductProjectionSearch(userContext.locale(), searchTerm), boundFacets, boundSortOptions, page);
final F.Promise<CmsPage> cmsPromise = cmsService().getPage(userContext.locale(), "pop");
return searchResultPromise.flatMap(searchResult ->
cmsPromise.map(cms -> {
final ProductOverviewPageContent content = getPopPageData(userContext, messages, searchTerm, searchResult, boundFacets, page);
return ok(templateService().renderToHtml("pop", pageData(userContext, content)));
})
);
}
private List<Facet<ProductProjection>> boundFacetList(final Locale locale, final List<Category> childrenCategories, final Messages messages) {
final List<Category> subcategories = getCategoriesAsFlatList(categories(), childrenCategories);
final FacetOptionMapper categoryHierarchyMapper = HierarchicalCategoryFacetOptionMapper.of(subcategories, singletonList(locale));
final FacetOptionMapper sortedColorFacetOptionMapper = SortedFacetOptionMapper.of(emptyList());
final FacetOptionMapper sortedSizeFacetOptionMapper = SortedFacetOptionMapper.of(emptyList());
final List<Facet<ProductProjection>> facets = asList(
FlexibleSelectFacetBuilder.of(FACET_CATEGORY_KEY, messages.at("pop.facetProductType"), HIERARCHICAL_SELECT, CATEGORY_SEARCH_MODEL, categoryHierarchyMapper).build(),
FlexibleSelectFacetBuilder.of(FACET_SIZE_KEY, messages.at("pop.facetSize"), SORTED_SELECT, SIZE_SEARCH_MODEL, sortedSizeFacetOptionMapper).countHidden(true).build(),
FlexibleSelectFacetBuilder.of(FACET_COLOR_KEY, messages.at("pop.facetColor"), SORTED_SELECT, COLOR_SEARCH_MODEL, sortedColorFacetOptionMapper).build(),
SelectFacetBuilder.of(FACET_BRAND_KEY, messages.at("pop.facetBrand"), BRAND_SEARCH_MODEL).build());
return bindFacetsWithRequest(facets);
}
private List<SortOption<ProductProjection>> boundSortOptionList(final Messages messages) {
final List<SortOption<ProductProjection>> sortOptions = asList(
SortOptionImpl.of(messages.at("pop.sortNew"), SORT_NEW_KEY, true, MODIFIED_SORT_BY_DESC),
SortOptionImpl.of(messages.at("pop.sortPriceAsc"), SORT_PRICE_ASC_KEY, false, PRICE_SORT_BY_ASC),
SortOptionImpl.of(messages.at("pop.sortPriceDesc"), SORT_PRICE_DESC_KEY, false, PRICE_SORT_BY_DESC));
return bindSortOptionsWithRequest(sortOptions);
}
private ProductOverviewPageContent getPopPageData(final CmsPage cms, final UserContext userContext, final Messages messages,
final PagedSearchResult<ProductProjection> searchResult,
final List<Facet<ProductProjection>> boundFacets, final int currentPage,
final Reference<Category> category) {
final String additionalTitle = "";
final ProductOverviewPageStaticData staticData = new ProductOverviewPageStaticData(messages(userContext));
final List<SelectableLinkData> breadcrumbData = getBreadcrumbData(userContext, category);
final ProductListData productListData = getProductListData(searchResult.getResults(), userContext);
final FilterListData filterListData = getFilterListData(searchResult, boundFacets);
final PaginationData paginationData = getPaginationData(searchResult, currentPage);
final List<SortOption<ProductProjection>> sortingData = boundSortOptionList(messages);
return new ProductOverviewPageContent(additionalTitle, staticData, breadcrumbData, productListData, filterListData, sortingData, paginationData);
}
private ProductOverviewPageContent getPopPageData(final UserContext userContext,
final Messages messages,
final String searchTerm,
final PagedSearchResult<ProductProjection> searchResult,
final List<Facet<ProductProjection>> boundFacets,
final int currentPage) {
final String additionalTitle = "";
final ProductOverviewPageStaticData staticData = new ProductOverviewPageStaticData(messages(userContext));
final List<SelectableLinkData> breadcrumbData = getSearchBreadCrumbData(messages(userContext), userContext.locale().getLanguage(), searchTerm);
final ProductListData productListData = getProductListData(searchResult.getResults(), userContext);
final FilterListData filterListData = getFilterListData(searchResult, boundFacets);
final List<SortOption<ProductProjection>> sortingData = boundSortOptionList(messages);
final PaginationData paginationData = getPaginationData(searchResult, currentPage);
return new ProductOverviewPageContent(additionalTitle, staticData, breadcrumbData, productListData, filterListData, sortingData, paginationData);
}
/* Maybe move to some common controller class */
private static <T> List<Facet<T>> bindFacetsWithRequest(final List<Facet<T>> facets) {
return facets.stream().map(facet -> {
final List<String> selectedValues = asList(request().queryString().getOrDefault(facet.getKey(), new String[0]));
return facet.withSelectedValues(selectedValues);
}).collect(toList());
}
private static <T> List<SortOption<T>> bindSortOptionsWithRequest(final List<SortOption<T>> sortOptions) {
final String sortCriteria = Optional.ofNullable(request().getQueryString("sort")).orElse("");
return sortOptions.stream()
.map(option -> {
final boolean isSelected = sortCriteria.equals(option.getValue());
return option.withSelected(isSelected);
})
.collect(toList());
}
/* Move to product service */
private ProductProjectionSearch getProductProjectionSearch(final Category category) {
final List<String> categoriesId = getCategoriesAsFlatList(categories(), singletonList(category)).stream().map(Category::getId).collect(toList());
return ProductProjectionSearch.ofCurrent()
.withQueryFilters(model -> model.categories().id().filtered().by(categoriesId));
}
private ProductProjectionSearch getProductProjectionSearch(final Locale locale, final String searchTerm) {
return ProductProjectionSearch.ofCurrent().withText(locale, searchTerm);
}
private F.Promise<PagedSearchResult<ProductProjection>> searchProducts(final ProductProjectionSearch searchRequest,
final List<Facet<ProductProjection>> boundFacets,
final List<SortOption<ProductProjection>> sortOptions,
final int page) {
final int offset = (page - 1) * pageSize;
final ProductProjectionSearch facetedSearchRequest = getFacetedSearchRequest(searchRequest.withOffset(offset).withLimit(pageSize), boundFacets);
final ProductProjectionSearch sortedFacetedSearchRequest = getSortedSearchRequest(facetedSearchRequest, sortOptions);
final F.Promise<PagedSearchResult<ProductProjection>> searchResultPromise = sphere().execute(facetedSearchRequest);
searchResultPromise.onRedeem(result -> Logger.debug("Fetched {} out of {} products with request {}",
result.size(),
result.getTotal(),
sortedFacetedSearchRequest.httpRequestIntent().getPath()));
return searchResultPromise;
}
private static List<Category> getCategoriesAsFlatList(final CategoryTree categoryTree, final List<Category> parentCategories) {
final List<Category> categories = new ArrayList<>();
parentCategories.stream().forEach(parent -> {
categories.add(parent);
final List<Category> children = categoryTree.findChildren(parent);
categories.addAll(getCategoriesAsFlatList(categoryTree, children));
});
return categories;
}
/* Maybe export it to generic FacetedSearch class */
private static <T, S extends MetaModelSearchDsl<T, S, M, E>, M, E> S getFacetedSearchRequest(final S baseSearchRequest,
final List<Facet<T>> facets) {
S searchRequest = baseSearchRequest;
for (final Facet<T> facet : facets) {
final List<FilterExpression<T>> filterExpressions = facet.getFilterExpressions();
searchRequest = searchRequest
.plusFacets(facet.getFacetExpression())
.plusFacetFilters(filterExpressions)
.plusResultFilters(filterExpressions);
}
return searchRequest;
}
private static <T, S extends MetaModelSearchDsl<T, S, M, E>, M, E> S getSortedSearchRequest(final S searchRequest,
final List<SortOption<T>> sortOptions) {
return sortOptions.stream()
.filter(SortOption::isSelected)
.findFirst()
.map(option -> searchRequest.withSort(option.getSortModel()))
.orElse(searchRequest);
}
/* This will probably be moved to some kind of factory classes */
private List<SelectableLinkData> getBreadcrumbData(final UserContext userContext, final Reference<Category> category) {
final BreadcrumbDataFactory breadcrumbDataFactory = BreadcrumbDataFactory.of(reverseRouter(), userContext.locale());
final List<Category> breadcrumbCategories = categoryService.getBreadCrumbCategories(category);
return breadcrumbDataFactory.create(breadcrumbCategories);
}
private List<SelectableLinkData> getSearchBreadCrumbData(final Messages messages, final String languageTag, final String searchTerm) {
return asList(
new SelectableLinkData(messages.at("home.pageName"), reverseRouter().home(languageTag).url(), false),
new SelectableLinkData(messages.at("search.resultsForText", searchTerm), reverseRouter().search(languageTag, searchTerm, 1).url(), true)
);
}
private ProductListData getProductListData(final List<ProductProjection> productList, final UserContext userContext) {
final ProductDataFactory productDataFactory = ProductDataFactory.of(userContext, reverseRouter());
final List<ProductData> productDataList = productList.stream()
.map(product -> productDataFactory.create(product, product.getMasterVariant()))
.collect(toList());
return new ProductListData(productDataList);
}
private <T> FilterListData getFilterListData(final PagedSearchResult<T> searchResult, final List<Facet<T>> boundFacets) {
final List<FacetData> facets = boundFacets.stream()
.map(facet -> facet.withSearchResult(searchResult))
.map(FacetData::new)
.collect(toList());
return new FilterListData(request().uri(), facets);
}
private PaginationData getPaginationData(final PagedSearchResult<ProductProjection> searchResult, int currentPage) {
return new PaginationDataFactory(request(), searchResult, currentPage, pageSize, displayedPages).create();
}
}