diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentContainerImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentContainerImpl.java new file mode 100644 index 0000000000..83238e2247 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentContainerImpl.java @@ -0,0 +1,42 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment; + +import org.apache.sling.api.resource.Resource; + +import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragmentContainer; + +public class CommerceExperienceFragmentContainerImpl implements CommerceExperienceFragmentContainer { + + private Resource renderResource; + private String cssClassName; + + public CommerceExperienceFragmentContainerImpl(Resource renderResource, String cssClassName) { + this.renderResource = renderResource; + this.cssClassName = cssClassName; + } + + @Override + public Resource getRenderResource() { + return renderResource; + } + + @Override + public String getCssClassName() { + return cssClassName; + } + +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImpl.java index 0172d83aff..faf679f40c 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImpl.java @@ -15,20 +15,10 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment; -import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.RangeIterator; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; @@ -39,20 +29,14 @@ import org.apache.sling.models.annotations.injectorspecific.Self; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.models.common.SiteStructure; import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragment; import com.adobe.cq.commerce.core.components.services.urls.UrlProvider; -import com.day.cq.wcm.api.LanguageManager; import com.day.cq.wcm.api.Page; import com.day.cq.wcm.api.PageManager; -import com.day.cq.wcm.api.WCMException; -import com.day.cq.wcm.msm.api.LiveCopy; -import com.day.cq.wcm.msm.api.LiveRelationship; -import com.day.cq.wcm.msm.api.LiveRelationshipManager; @Model( adaptables = SlingHttpServletRequest.class, @@ -60,9 +44,7 @@ resourceType = CommerceExperienceFragmentImpl.RESOURCE_TYPE) public class CommerceExperienceFragmentImpl implements CommerceExperienceFragment { - protected static final String RESOURCE_TYPE = "core/cif/components/commerce/experiencefragment/v1/experiencefragment"; - private static final Logger LOGGER = LoggerFactory.getLogger(CommerceExperienceFragmentImpl.class); - private static final String XF_ROOT = "/content/experience-fragments/"; + public static final String RESOURCE_TYPE = "core/cif/components/commerce/experiencefragment/v1/experiencefragment"; @Self private SlingHttpServletRequest request; @@ -86,10 +68,7 @@ public class CommerceExperienceFragmentImpl implements CommerceExperienceFragmen private UrlProvider urlProvider; @OSGiService - private LanguageManager languageManager; - - @OSGiService - private LiveRelationshipManager relationshipManager; + private CommerceExperienceFragmentsRetriever fragmentsRetriever; @Self private SiteStructure siteNavigation; @@ -99,113 +78,21 @@ public class CommerceExperienceFragmentImpl implements CommerceExperienceFragmen @PostConstruct private void initModel() { - String query = null; + List xfs = null; if (siteNavigation.isProductPage(currentPage)) { - query = getQueryForProduct(); + String sku = urlProvider.getProductIdentifier(request); + xfs = fragmentsRetriever.getExperienceFragmentsForProduct(sku, fragmentLocation, currentPage); } else if (siteNavigation.isCategoryPage(currentPage)) { - query = getQueryForCategory(); - } - - if (query == null) { - return; + String categoryUid = urlProvider.getCategoryIdentifier(request); + xfs = fragmentsRetriever.getExperienceFragmentsForCategory(categoryUid, fragmentLocation, currentPage); } - List xfs = findExperienceFragments(query); - if (!xfs.isEmpty()) { + if (xfs != null && !xfs.isEmpty()) { xfResource = xfs.get(0); resolveName(); } } - private String getQueryForProduct() { - // Extract product sku from request URL - String sku = urlProvider.getProductIdentifier(request); - - if (StringUtils.isBlank(sku)) { - LOGGER.warn("Cannot find product for current request"); - return null; - } - - return buildQueryForProduct(sku); - } - - private String buildQueryForProduct(String sku) { - // This query is backed up by an index - final String PRODUCT_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s') " - + "AND (node.[" + PN_CQ_PRODUCTS + "] = '%s' OR node.[" + PN_CQ_PRODUCTS + "] LIKE '%s#%%') " - + "AND node.[" + PN_FRAGMENT_LOCATION + "] "; - - String query = String.format(PRODUCT_QUERY_TEMPLATE, getExperienceFragmentsRoot(), sku, sku); - if (fragmentLocation != null) { - query += "= '" + fragmentLocation + "'"; - } else { - query += "IS NULL"; - } - - return query; - } - - private String getQueryForCategory() { - // Extract category uid sku from request URL - String categoryUid = urlProvider.getCategoryIdentifier(request); - - if (StringUtils.isBlank(categoryUid)) { - LOGGER.warn("Cannot find category for current request"); - return null; - } - - return buildQueryForCategory(categoryUid); - } - - private String buildQueryForCategory(String categoryId) { - final String CATEGORY_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s') " - + "AND node.[" + PN_CQ_CATEGORIES + "] = '%s' " - + "AND node.[" + PN_FRAGMENT_LOCATION + "] "; - - String query = String.format(CATEGORY_QUERY_TEMPLATE, getExperienceFragmentsRoot(), categoryId); - if (fragmentLocation != null) { - query += "= '" + fragmentLocation + "'"; - } else { - query += "IS NULL"; - } - - return query; - } - - private String getExperienceFragmentsRoot() { - String localizationRoot = getLocalizationRoot(currentPage.getPath()); - return localizationRoot != null ? localizationRoot.replace("/content/", XF_ROOT) : XF_ROOT; - } - - private List findExperienceFragments(String query) { - LOGGER.debug("Looking for experience fragments with query: {}", query); - - List experienceFragments = new ArrayList<>(); - try { - Session session = resolver.adaptTo(Session.class); - Workspace workspace = session.getWorkspace(); - QueryManager qm = workspace.getQueryManager(); - Query jcrQuery = qm.createQuery(query, "JCR-SQL2"); - QueryResult result = jcrQuery.execute(); - NodeIterator nodes = result.getNodes(); - while (nodes.hasNext()) { - Node node = nodes.nextNode(); - Resource resource = resolver.getResource(node.getPath()); - if (resource != null) { - experienceFragments.add(resource); - } - } - } catch (Exception e) { - LOGGER.error("Error looking for experience fragments", e); - } - - if (experienceFragments.size() > 1) { - LOGGER.warn("Found multiple experience fragments matching {} with location {}", request.getRequestURI(), fragmentLocation); - } - - return experienceFragments; - } - private void resolveName() { PageManager pageManager = resolver.adaptTo(PageManager.class); Page xfVariationPage = pageManager.getPage(xfResource.getParent().getPath()); @@ -231,91 +118,4 @@ public String getName() { public String getExportedType() { return resource.getResourceType(); } - - // All the methods below are copied from the WCM ExperienceFragmentImpl class - // and will be OSGi-exported in a new public class in a next release - - /** - * Returns the localization root of the resource defined at the given path. - * - * @param path the resource path - * @return the localization root of the resource at the given path if it exists, {@code null} otherwise - */ - private String getLocalizationRoot(String path) { - String root = null; - if (StringUtils.isNotEmpty(path)) { - Resource resource = resolver.getResource(path); - root = getLanguageRoot(resource); - if (StringUtils.isEmpty(root)) { - root = getBlueprintPath(resource); - } - if (StringUtils.isEmpty(root)) { - root = getLiveCopyPath(resource); - } - } - return root; - } - - /** - * Returns the language root of the resource. - * - * @param resource the resource - * @return the language root of the resource if it exists, {@code null} otherwise - */ - private String getLanguageRoot(Resource resource) { - Page rootPage = languageManager.getLanguageRoot(resource); - if (rootPage != null) { - return rootPage.getPath(); - } - return null; - } - - /** - * Returns the path of the blueprint of the resource. - * - * @param resource the resource - * @return the path of the blueprint of the resource if it exists, {@code null} otherwise - */ - private String getBlueprintPath(Resource resource) { - try { - if (relationshipManager.isSource(resource)) { - // the resource is a blueprint - RangeIterator liveCopiesIterator = relationshipManager.getLiveRelationships(resource, null, null); - if (liveCopiesIterator != null) { - LiveRelationship relationship = (LiveRelationship) liveCopiesIterator.next(); - LiveCopy liveCopy = relationship.getLiveCopy(); - if (liveCopy != null) { - return liveCopy.getBlueprintPath(); - } - } - } - } catch (WCMException e) { - LOGGER.error("Unable to get the blueprint: {}", e.getMessage()); - } - return null; - } - - /** - * Returns the path of the live copy of the resource. - * - * @param resource the resource - * @return the path of the live copy of the resource if it exists, {@code null} otherwise - */ - private String getLiveCopyPath(Resource resource) { - try { - if (relationshipManager.hasLiveRelationship(resource)) { - // the resource is a live copy - LiveRelationship liveRelationship = relationshipManager.getLiveRelationship(resource, false); - if (liveRelationship != null) { - LiveCopy liveCopy = liveRelationship.getLiveCopy(); - if (liveCopy != null) { - return liveCopy.getPath(); - } - } - } - } catch (WCMException e) { - LOGGER.error("Unable to get the live copy: {}", e.getMessage()); - } - return null; - } } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImpl.java index 42f6817dba..66149fabdc 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImpl.java @@ -16,6 +16,7 @@ package com.adobe.cq.commerce.core.components.internal.models.v1.productlist; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -33,9 +34,12 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.scripting.SlingScriptHelper; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.ScriptVariable; import org.apache.sling.models.annotations.injectorspecific.Self; import org.apache.sling.models.annotations.injectorspecific.SlingObject; @@ -44,10 +48,14 @@ import org.slf4j.LoggerFactory; import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient; +import com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment.CommerceExperienceFragmentContainerImpl; +import com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment.CommerceExperienceFragmentImpl; import com.adobe.cq.commerce.core.components.internal.models.v1.productcollection.ProductCollectionImpl; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.internal.services.sitemap.SitemapLinkExternalizerProvider; import com.adobe.cq.commerce.core.components.internal.storefrontcontext.CategoryStorefrontContextImpl; import com.adobe.cq.commerce.core.components.models.common.ProductListItem; +import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragmentContainer; import com.adobe.cq.commerce.core.components.models.productlist.ProductList; import com.adobe.cq.commerce.core.components.models.retriever.AbstractCategoryRetriever; import com.adobe.cq.commerce.core.components.services.urls.CategoryUrlFormat; @@ -65,14 +73,15 @@ import com.adobe.cq.commerce.magento.graphql.CategoryTree; import com.adobe.cq.commerce.magento.graphql.ProductInterfaceQuery; import com.adobe.cq.sightly.SightlyWCMMode; +import com.adobe.granite.ui.components.ValueMapResourceWrapper; -@Model( - adaptables = SlingHttpServletRequest.class, - adapters = ProductList.class, - resourceType = ProductListImpl.RESOURCE_TYPE) +@Model(adaptables = SlingHttpServletRequest.class, adapters = ProductList.class, resourceType = ProductListImpl.RESOURCE_TYPE) public class ProductListImpl extends ProductCollectionImpl implements ProductList { public static final String RESOURCE_TYPE = "core/cif/components/commerce/productlist/v1/productlist"; + public static final String PN_FRAGMENT_LOCATION = "fragmentLocation"; + public static final String PN_FRAGMENT_CSS_CLASS = "fragmentCssClass"; + public static final String PN_FRAGMENT_PAGE = "fragmentPage"; protected static final String PLACEHOLDER_DATA = "productlist-component-placeholder-data.json"; private static final Logger LOGGER = LoggerFactory.getLogger(ProductListImpl.class); @@ -85,7 +94,8 @@ public class ProductListImpl extends ProductCollectionImpl implements ProductLis private boolean showTitle; private boolean showImage; - // This script variable is not injected when the model is instantiated in SpecificPageServlet + // This script variable is not injected when the model is instantiated in + // SpecificPageServlet @ScriptVariable(name = "wcmmode", injectionStrategy = InjectionStrategy.OPTIONAL) private SightlyWCMMode wcmMode = null; @Self(injectionStrategy = InjectionStrategy.OPTIONAL) @@ -95,6 +105,9 @@ public class ProductListImpl extends ProductCollectionImpl implements ProductLis @ValueMapValue(name = CATEGORY_PROPERTY, injectionStrategy = InjectionStrategy.OPTIONAL) private String categoryUid; + @OSGiService + private CommerceExperienceFragmentsRetriever fragmentsRetriever; + protected AbstractCategoryRetriever categoryRetriever; private boolean usePlaceholderData; private boolean isAuthor; @@ -102,6 +115,8 @@ public class ProductListImpl extends ProductCollectionImpl implements ProductLis private Pair categorySearchResultsSet; + protected List fragments = new ArrayList<>(); + @PostConstruct protected void initModel() { if (properties == null) { @@ -113,13 +128,15 @@ protected void initModel() { isAuthor = wcmMode != null && !wcmMode.isDisabled(); String currentPageIndexCandidate = request.getParameter(SearchOptionsImpl.CURRENT_PAGE_PARAMETER_ID); - // make sure the current page from the query string is reasonable i.e. numeric and over 0 + // make sure the current page from the query string is reasonable i.e. numeric + // and over 0 Integer currentPageIndex = calculateCurrentPageCursor(currentPageIndexCandidate); Map searchFilters = createFilterMap(request.getParameterMap()); if (StringUtils.isBlank(categoryUid)) { - // If not provided via the category property extract category identifier from URL + // If not provided via the category property extract category identifier from + // URL categoryUid = urlProvider.getCategoryIdentifier(request); } @@ -146,6 +163,30 @@ protected void initModel() { if (usePlaceholderData) { searchResultsSet = new SearchResultsSetImpl(); } else { + Resource fragmentsNode = resource.getChild(ProductList.NN_FRAGMENTS); + if (fragmentsNode != null && fragmentsNode.hasChildren()) { + Iterable configuredFragments = fragmentsNode.getChildren(); + for (Resource fragment : configuredFragments) { + ValueMap fragmentVm = fragment.getValueMap(); + Integer fragmentPage = fragmentVm.get(PN_FRAGMENT_PAGE, -1); + if (fragmentPage.equals(currentPageIndex)) { + String fragmentCssClass = fragment.getValueMap().get(PN_FRAGMENT_CSS_CLASS, String.class); + ValueMapResourceWrapper resourceWrapper = new ValueMapResourceWrapper( + fragment, + CommerceExperienceFragmentImpl.RESOURCE_TYPE); + String fragmentLocation = fragment.getValueMap().get(PN_FRAGMENT_LOCATION, + String.class); + resourceWrapper.getValueMap().put(PN_FRAGMENT_LOCATION, fragmentLocation); + if (!fragmentsRetriever + .getExperienceFragmentsForCategory(categoryUid, fragmentLocation, currentPage) + .isEmpty()) { + fragments.add(new CommerceExperienceFragmentContainerImpl(resourceWrapper, + fragmentCssClass)); + } + } + } + } + searchOptions = new SearchOptionsImpl(); searchOptions.setCurrentPage(currentPageIndex); searchOptions.setPageSize(navPageSize); @@ -196,7 +237,8 @@ public Collection getProducts() { if (usePlaceholderData) { CategoryInterface category = getCategory(); CategoryProducts categoryProducts = category.getProducts(); - ProductToProductListItemConverter converter = new ProductToProductListItemConverter(currentPage, request, urlProvider, getId(), + ProductToProductListItemConverter converter = new ProductToProductListItemConverter(currentPage, request, + urlProvider, getId(), category); return categoryProducts.getItems().stream() .map(converter) @@ -207,6 +249,11 @@ public Collection getProducts() { } } + @Override + public List getExperienceFragments() { + return fragments; + } + @Nonnull @Override public SearchResultsSet getSearchResultsSet() { @@ -215,7 +262,8 @@ public SearchResultsSet getSearchResultsSet() { List searchAggregations = searchResultsSet.getSearchAggregations() .stream() - .filter(searchAggregation -> !SearchOptionsImpl.CATEGORY_UID_PARAMETER_ID.equals(searchAggregation.getIdentifier())) + .filter(searchAggregation -> !SearchOptionsImpl.CATEGORY_UID_PARAMETER_ID + .equals(searchAggregation.getIdentifier())) .collect(Collectors.toList()); CategoryTree categoryTree = (CategoryTree) getCategorySearchResultsSet().getLeft(); @@ -229,43 +277,51 @@ public SearchResultsSet getSearchResultsSet() { private void processCategoryAggregation(List searchAggregations, CategoryTree categoryTree) { if (categoryTree != null && categoryTree.getChildren() != null) { List childCategories = categoryTree.getChildren(); - searchAggregations.stream().filter(aggregation -> CATEGORY_AGGREGATION_ID.equals(aggregation.getIdentifier())).findAny() + searchAggregations.stream() + .filter(aggregation -> CATEGORY_AGGREGATION_ID.equals(aggregation.getIdentifier())).findAny() .ifPresent(categoryAggregation -> { List options = categoryAggregation.getOptions(); - // find and process category aggregation options related to child categories of current category + // find and process category aggregation options related to child categories of + // current category List filteredOptions = options.stream().map( - option -> option instanceof SearchAggregationOptionImpl ? (SearchAggregationOptionImpl) option - : new SearchAggregationOptionImpl(option)).filter(option -> { - Optional categoryRef = childCategories.stream().filter(c -> String.valueOf(c.getId()).equals( - option.getFilterValue())).findAny(); - - if (categoryRef.isPresent()) { - CategoryTree category = categoryRef.get(); - CategoryUrlFormat.Params params = new CategoryUrlFormat.Params(); - params.setUid(category.getUid().toString()); - params.setUrlKey(category.getUrlKey()); - params.setUrlPath(category.getUrlPath()); - option.setPageUrl(urlProvider.toCategoryUrl(request, currentPage, params)); - option.getAddFilterMap().remove(CATEGORY_AGGREGATION_ID); - return true; - } else { - return false; - } - }).collect(Collectors.toList()); - - // keep filtered options only or remove category aggregation if no option was found + option -> option instanceof SearchAggregationOptionImpl + ? (SearchAggregationOptionImpl) option + : new SearchAggregationOptionImpl(option)) + .filter(option -> { + Optional categoryRef = childCategories.stream() + .filter(c -> String.valueOf(c.getId()).equals( + option.getFilterValue())) + .findAny(); + + if (categoryRef.isPresent()) { + CategoryTree category = categoryRef.get(); + CategoryUrlFormat.Params params = new CategoryUrlFormat.Params(); + params.setUid(category.getUid().toString()); + params.setUrlKey(category.getUrlKey()); + params.setUrlPath(category.getUrlPath()); + option.setPageUrl(urlProvider.toCategoryUrl(request, currentPage, params)); + option.getAddFilterMap().remove(CATEGORY_AGGREGATION_ID); + return true; + } else { + return false; + } + }).collect(Collectors.toList()); + + // keep filtered options only or remove category aggregation if no option was + // found if (filteredOptions.isEmpty()) { searchAggregations.removeIf(a -> CATEGORY_AGGREGATION_ID.equals(a.getIdentifier())); } else { options.clear(); options.addAll(filteredOptions); // move category aggregation to front - searchAggregations.stream().filter(a -> CATEGORY_AGGREGATION_ID.equals(a.getIdentifier())).findAny().ifPresent( - aggregation -> { - searchAggregations.remove(aggregation); - searchAggregations.add(0, aggregation); - }); + searchAggregations.stream().filter(a -> CATEGORY_AGGREGATION_ID.equals(a.getIdentifier())) + .findAny().ifPresent( + aggregation -> { + searchAggregations.remove(aggregation); + searchAggregations.add(0, aggregation); + }); } }); } else { @@ -275,7 +331,9 @@ private void processCategoryAggregation(List searchAggregatio private Pair getCategorySearchResultsSet() { if (categorySearchResultsSet == null) { - Consumer productQueryHook = categoryRetriever != null ? categoryRetriever.getProductQueryHook() : null; + Consumer productQueryHook = categoryRetriever != null + ? categoryRetriever.getProductQueryHook() + : null; categorySearchResultsSet = searchResultsService .performSearch(searchOptions, resource, currentPage, request, productQueryHook, categoryRetriever); } @@ -317,7 +375,8 @@ public String getCanonicalUrl() { } if (canonicalUrl == null) { CategoryInterface category = getCategory(); - SitemapLinkExternalizerProvider sitemapLinkExternalizerProvider = sling.getService(SitemapLinkExternalizerProvider.class); + SitemapLinkExternalizerProvider sitemapLinkExternalizerProvider = sling + .getService(SitemapLinkExternalizerProvider.class); if (category != null && sitemapLinkExternalizerProvider != null) { canonicalUrl = sitemapLinkExternalizerProvider.getExternalizer(request.getResourceResolver()) diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetriever.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetriever.java new file mode 100644 index 0000000000..7b3254e9c2 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetriever.java @@ -0,0 +1,48 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.services.experiencefragments; + +import java.util.List; + +import org.apache.sling.api.resource.Resource; +import org.osgi.annotation.versioning.ProviderType; + +import com.day.cq.wcm.api.Page; + +/** + * This service searches for experience fragments associated + * with a product or category + */ +@ProviderType +public interface CommerceExperienceFragmentsRetriever { + + /** + * This method returns a list of experience fragments that match the location + * and product identifier. + * + * @return The a list of experience fragments that match this container. + */ + List getExperienceFragmentsForProduct(String sku, String fragmentLocation, Page currentPage); + + /** + * This method returns a list of experience fragments that match the location + * and category identifier. + * + * @return The a list of experience fragments that match this container. + */ + List getExperienceFragmentsForCategory(String categoryUid, String fragmentLocation, Page currentPage); + +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverImpl.java new file mode 100644 index 0000000000..c3b4427b44 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverImpl.java @@ -0,0 +1,231 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.services.experiencefragments; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RangeIterator; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragment; +import com.day.cq.wcm.api.LanguageManager; +import com.day.cq.wcm.api.Page; +import com.day.cq.wcm.api.WCMException; +import com.day.cq.wcm.msm.api.LiveCopy; +import com.day.cq.wcm.msm.api.LiveRelationship; +import com.day.cq.wcm.msm.api.LiveRelationshipManager; + +@Component(service = { CommerceExperienceFragmentsRetriever.class, CommerceExperienceFragmentsRetrieverImpl.class }) +public class CommerceExperienceFragmentsRetrieverImpl implements CommerceExperienceFragmentsRetriever { + private static final Logger LOGGER = LoggerFactory.getLogger(CommerceExperienceFragmentsRetrieverImpl.class); + private static final String XF_ROOT = "/content/experience-fragments/"; + + @Reference + private LanguageManager languageManager; + + @Reference + private LiveRelationshipManager relationshipManager; + + @Override + public List getExperienceFragmentsForProduct(String sku, String fragmentLocation, Page currentPage) { + if (StringUtils.isBlank(sku)) { + LOGGER.warn("Cannot find product for current request"); + return Collections.emptyList(); + } + + String query = buildQueryForProduct(sku, fragmentLocation, currentPage); + return findExperienceFragments(query, currentPage.getContentResource().getResourceResolver()); + } + + @Override + public List getExperienceFragmentsForCategory(String categoryUid, String fragmentLocation, + Page currentPage) { + if (StringUtils.isBlank(categoryUid)) { + LOGGER.warn("Cannot find category for current request"); + return Collections.emptyList(); + } + + String query = buildQueryForCategory(categoryUid, fragmentLocation, currentPage); + return findExperienceFragments(query, currentPage.getContentResource().getResourceResolver()); + } + + private String buildQueryForProduct(String sku, String fragmentLocation, Page currentPage) { + // This query is backed up by an index + final String PRODUCT_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s') " + + "AND (node.[" + CommerceExperienceFragment.PN_CQ_PRODUCTS + "] = '%s' OR node.[" + CommerceExperienceFragment.PN_CQ_PRODUCTS + + "] LIKE '%s#%%') " + + "AND node.[" + CommerceExperienceFragment.PN_FRAGMENT_LOCATION + "] "; + + String query = String.format(PRODUCT_QUERY_TEMPLATE, getExperienceFragmentsRoot(currentPage), sku, sku); + if (fragmentLocation != null) { + query += "= '" + fragmentLocation + "'"; + } else { + query += "IS NULL"; + } + + return query; + } + + private String buildQueryForCategory(String categoryId, String fragmentLocation, Page currentPage) { + final String CATEGORY_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s') " + + "AND node.[" + CommerceExperienceFragment.PN_CQ_CATEGORIES + "] = '%s' " + + "AND node.[" + CommerceExperienceFragment.PN_FRAGMENT_LOCATION + "] "; + + String query = String.format(CATEGORY_QUERY_TEMPLATE, getExperienceFragmentsRoot(currentPage), categoryId); + if (fragmentLocation != null) { + query += "= '" + fragmentLocation + "'"; + } else { + query += "IS NULL"; + } + + return query; + } + + private String getExperienceFragmentsRoot(Page currentPage) { + String localizationRoot = getLocalizationRoot(currentPage.getPath(), currentPage.getContentResource().getResourceResolver()); + return localizationRoot != null ? localizationRoot.replace("/content/", XF_ROOT) : XF_ROOT; + } + + private List findExperienceFragments(String query, ResourceResolver resolver) { + LOGGER.debug("Looking for experience fragments with query: {}", query); + + List experienceFragments = new ArrayList<>(); + try { + Session session = resolver.adaptTo(Session.class); + Workspace workspace = session.getWorkspace(); + QueryManager qm = workspace.getQueryManager(); + Query jcrQuery = qm.createQuery(query, "JCR-SQL2"); + QueryResult result = jcrQuery.execute(); + NodeIterator nodes = result.getNodes(); + while (nodes.hasNext()) { + Node node = nodes.nextNode(); + Resource resource = resolver.getResource(node.getPath()); + if (resource != null) { + experienceFragments.add(resource); + } + } + } catch (Exception e) { + LOGGER.error("Error looking for experience fragments", e); + } + + return experienceFragments; + } + + /** + * Returns the localization root of the resource defined at the given path. + * + * @param path the resource path + * @return the localization root of the resource at the given path if it exists, + * {@code null} otherwise + */ + private String getLocalizationRoot(String path, ResourceResolver resolver) { + String root = null; + if (StringUtils.isNotEmpty(path)) { + Resource resource = resolver.getResource(path); + root = getLanguageRoot(resource); + if (StringUtils.isEmpty(root)) { + root = getBlueprintPath(resource); + } + if (StringUtils.isEmpty(root)) { + root = getLiveCopyPath(resource); + } + } + return root; + } + + /** + * Returns the language root of the resource. + * + * @param resource the resource + * @return the language root of the resource if it exists, {@code null} + * otherwise + */ + private String getLanguageRoot(Resource resource) { + Page rootPage = languageManager.getLanguageRoot(resource); + if (rootPage != null) { + return rootPage.getPath(); + } + return null; + } + + /** + * Returns the path of the blueprint of the resource. + * + * @param resource the resource + * @return the path of the blueprint of the resource if it exists, {@code null} + * otherwise + */ + private String getBlueprintPath(Resource resource) { + try { + if (relationshipManager.isSource(resource)) { + // the resource is a blueprint + RangeIterator liveCopiesIterator = relationshipManager.getLiveRelationships(resource, null, null); + if (liveCopiesIterator != null) { + LiveRelationship relationship = (LiveRelationship) liveCopiesIterator.next(); + LiveCopy liveCopy = relationship.getLiveCopy(); + if (liveCopy != null) { + return liveCopy.getBlueprintPath(); + } + } + } + } catch (WCMException e) { + LOGGER.error("Unable to get the blueprint: {}", e.getMessage()); + } + return null; + } + + /** + * Returns the path of the live copy of the resource. + * + * @param resource the resource + * @return the path of the live copy of the resource if it exists, {@code null} + * otherwise + */ + private String getLiveCopyPath(Resource resource) { + try { + if (relationshipManager.hasLiveRelationship(resource)) { + // the resource is a live copy + LiveRelationship liveRelationship = relationshipManager.getLiveRelationship(resource, false); + if (liveRelationship != null) { + LiveCopy liveCopy = liveRelationship.getLiveCopy(); + if (liveCopy != null) { + return liveCopy.getPath(); + } + } + } + } catch (WCMException e) { + LOGGER.error("Unable to get the live copy: {}", e.getMessage()); + } + return null; + } + +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/AbstractProductListXfServlet.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/AbstractProductListXfServlet.java new file mode 100644 index 0000000000..ade5e3822e --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/AbstractProductListXfServlet.java @@ -0,0 +1,50 @@ +package com.adobe.cq.commerce.core.components.internal.servlets; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; + +import com.day.cq.wcm.api.policies.ContentPolicy; +import com.day.cq.wcm.api.policies.ContentPolicyManager; + +public abstract class AbstractProductListXfServlet extends SlingSafeMethodsServlet { + public static final String PN_FRAGMENT_STYLES = "fragmentStyles"; + + protected Resource getFragmentStylesResource(SlingHttpServletRequest request) { + ResourceResolver resolver = request.getResourceResolver(); + + Resource contentResource = request.getRequestPathInfo().getSuffixResource(); + + if (contentResource != null) { + ContentPolicyManager policyManager = resolver.adaptTo(ContentPolicyManager.class); + if (policyManager != null) { + ContentPolicy policy = policyManager.getPolicy(contentResource); + if (policy != null) { + String policyPath = policy.getPath(); + Resource policyResource = resolver.getResource(policyPath); + + return policyResource.getChild(PN_FRAGMENT_STYLES); + } + } + } + + return null; + } + +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServlet.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServlet.java new file mode 100644 index 0000000000..e86ed13111 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServlet.java @@ -0,0 +1,121 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.servlets; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.SyntheticResource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.api.wrappers.ValueMapDecorator; +import org.osgi.service.component.annotations.Component; + +import com.adobe.granite.ui.components.ds.DataSource; +import com.adobe.granite.ui.components.ds.SimpleDataSource; + +@Component( + service = { Servlet.class }, + property = { + "sling.servlet.resourceTypes=" + ProductListXfStylesDataSourceServlet.RESOURCE_TYPE_V1, + "sling.servlet.methods=GET", + "sling.servlet.extensions=html" + }) +public class ProductListXfStylesDataSourceServlet extends AbstractProductListXfServlet { + public static final String RESOURCE_TYPE_V1 = "core/cif/components/commerce/datasources/productlistxfstyles/v1"; + public static final String PN_FRAGMENT_STYLE_CLASS = "fragmentStyleCssClass"; + public static final String PN_FRAGMENT_STYLE_NAME = "fragmentStyleName"; + public static final String PN_COMPONENT_PATH = "componentPath"; + + @Override + protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) + throws ServletException, IOException { + SimpleDataSource xfStylesDataSource = new SimpleDataSource( + getXfStyles(request).iterator()); + request.setAttribute(DataSource.class.getName(), xfStylesDataSource); + } + + private List getXfStyles(SlingHttpServletRequest request) { + List xfStyles = new ArrayList<>(); + Resource fragmentStylesResource = getFragmentStylesResource(request); + if (fragmentStylesResource != null) { + fragmentStylesResource.getChildren().forEach(fs -> { + ValueMap vm = fs.getValueMap(); + String cssClassName = vm.get(PN_FRAGMENT_STYLE_CLASS, String.class); + String styleName = vm.get(PN_FRAGMENT_STYLE_NAME, String.class); + xfStyles.add(new XfStyleResource(cssClassName, styleName, request.getResourceResolver())); + }); + } + + return xfStyles; + } + + /** + * Synthetic resource for a product list experience fragment style that can be + * used by a + * Granite form select field. + */ + static class XfStyleResource extends SyntheticResource { + public static final String PN_VALUE = "value"; + public static final String PN_TEXT = "text"; + + private final String value; + private final String text; + private ValueMap valueMap; + + XfStyleResource(String value, String text, ResourceResolver resourceResolver) { + super(resourceResolver, StringUtils.EMPTY, RESOURCE_TYPE_NON_EXISTING); + this.value = value; + this.text = text; + } + + @Override + @SuppressWarnings("unchecked") + public AdapterType adaptTo(Class type) { + if (type == ValueMap.class) { + if (valueMap == null) { + initValueMap(); + } + return (AdapterType) valueMap; + } else { + return super.adaptTo(type); + } + } + + private void initValueMap() { + valueMap = new ValueMapDecorator(new HashMap()); + valueMap.put(PN_VALUE, getValue()); + valueMap.put(PN_TEXT, getText()); + } + + public String getText() { + return text; + } + + public String getValue() { + return value; + } + } +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServlet.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServlet.java new file mode 100644 index 0000000000..7b6ecfdbd0 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServlet.java @@ -0,0 +1,56 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.servlets; + +import java.io.IOException; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.osgi.service.component.annotations.Component; + +import com.adobe.granite.ui.components.rendercondition.RenderCondition; +import com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition; + +@Component( + service = { Servlet.class }, + property = { + "sling.servlet.resourceTypes=" + ProductListXfStylesRenderConditionServlet.RESOURCE_TYPE_V1, + "sling.servlet.methods=GET", + "sling.servlet.extensions=html" + }) +public class ProductListXfStylesRenderConditionServlet extends AbstractProductListXfServlet { + public static final String RESOURCE_TYPE_V1 = "core/cif/components/commerce/renderconditions/productlistxfstyles/v1"; + + @Override + protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) + throws ServletException, IOException { + request.setAttribute(RenderCondition.class.getName(), + new SimpleRenderCondition(checkXfStyles(request))); + } + + private boolean checkXfStyles(SlingHttpServletRequest request) { + Resource fragmentStylesResource = getFragmentStylesResource(request); + if (fragmentStylesResource != null) { + return fragmentStylesResource.hasChildren(); + } + + return false; + } +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/CommerceExperienceFragmentContainer.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/CommerceExperienceFragmentContainer.java new file mode 100644 index 0000000000..55ee11bbb5 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/CommerceExperienceFragmentContainer.java @@ -0,0 +1,36 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.models.experiencefragment; + +import org.apache.sling.api.resource.Resource; + +import com.adobe.cq.wcm.core.components.models.Component; + +/** + * Interface for an Experience Fragment container to be used in a + * {@code ProductList} + */ +public interface CommerceExperienceFragmentContainer extends Component { + + String getCssClassName(); + + /** + * Returns the ExperienceFragment resource to be rendered. + * + * @return the ExperienceFragment resource to be rendered + */ + Resource getRenderResource(); +} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/package-info.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/package-info.java index 02ffadb636..ae6bab382d 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/package-info.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/experiencefragment/package-info.java @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -@Version("1.12.0") +@Version("1.13.0") package com.adobe.cq.commerce.core.components.models.experiencefragment; import org.osgi.annotation.versioning.Version; \ No newline at end of file diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/ProductList.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/ProductList.java index e72167c6d1..eabcb0d775 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/ProductList.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/ProductList.java @@ -15,10 +15,14 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.models.productlist; +import java.util.Collections; +import java.util.List; + import javax.annotation.Nullable; import org.osgi.annotation.versioning.ConsumerType; +import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragmentContainer; import com.adobe.cq.commerce.core.components.models.page.PageMetadata; import com.adobe.cq.commerce.core.components.models.productcollection.ProductCollection; import com.adobe.cq.commerce.core.components.models.retriever.AbstractCategoryRetriever; @@ -38,6 +42,11 @@ public interface ProductList extends Component, ProductCollection, PageMetadata */ String PN_SHOW_IMAGE = "showImage"; + /** + * Name of the child node where the fragment elements are stored + */ + String NN_FRAGMENTS = "fragments"; + /** * Returns {@code true} if the category / product list title should be rendered. * @@ -82,4 +91,13 @@ default Boolean isStaged() { * @return context of the categories in the product list */ CategoryStorefrontContext getStorefrontContext(); + + /** + * Return the experience fragment resources that match the configured locations for the current category + * + * @return a list of experience fragment resources + */ + default List getExperienceFragments() { + return Collections.emptyList(); + } } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/package-info.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/package-info.java index 1d5c1fe07b..ed1da7036a 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/package-info.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/models/productlist/package-info.java @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -@Version("4.4.0") +@Version("4.5.0") package com.adobe.cq.commerce.core.components.models.productlist; import org.osgi.annotation.versioning.Version; \ No newline at end of file diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImplTest.java index 4f8e8ce7cc..c5c726debf 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/CommerceExperienceFragmentImplTest.java @@ -16,8 +16,7 @@ package com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment; import java.io.IOException; - -import javax.jcr.Session; +import java.util.Collections; import org.apache.http.HttpStatus; import org.apache.http.impl.client.CloseableHttpClient; @@ -27,19 +26,17 @@ import org.apache.sling.api.scripting.SlingBindings; import org.apache.sling.api.wrappers.ValueMapDecorator; import org.apache.sling.servlethelpers.MockRequestPathInfo; -import org.apache.sling.testing.mock.jcr.MockJcr; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; import com.adobe.cq.commerce.core.MockHttpClientBuilderFactory; -import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragment; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.commerce.core.testing.Utils; import com.adobe.cq.commerce.graphql.client.GraphqlClient; import com.adobe.cq.commerce.graphql.client.impl.GraphqlClientImpl; -import com.day.cq.wcm.api.LanguageManager; import com.day.cq.wcm.api.Page; import com.day.cq.wcm.msm.api.LiveRelationshipManager; import com.day.cq.wcm.scripting.WCMBindingsConstants; @@ -49,6 +46,8 @@ import static com.adobe.cq.commerce.core.testing.TestContext.buildAemContext; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -61,25 +60,33 @@ public class CommerceExperienceFragmentImplTest { private static final String ANOTHER_PAGE = PAGE + "/another-page"; private static final String RESOURCE_XF1 = "/jcr:content/root/xf-component-1"; private static final String RESOURCE_XF2 = "/jcr:content/root/xf-component-2"; - private static final String XF_ROOT = "/content/experience-fragments/"; - private static final String SITE_XF_ROOT = XF_ROOT + "mysite/page"; - - private static final String PRODUCT_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s')" + - " AND (node.[" + CommerceExperienceFragment.PN_CQ_PRODUCTS + "] = '%s'" + - " OR node.[" + CommerceExperienceFragment.PN_CQ_PRODUCTS + "] LIKE '%s#%%')" + - " AND node.[" + CommerceExperienceFragment.PN_FRAGMENT_LOCATION + "] %s"; - - private static final String CATEGORY_QUERY_TEMPLATE = "SELECT * FROM [cq:PageContent] as node WHERE ISDESCENDANTNODE('%s')" + - " AND node.[" + CommerceExperienceFragment.PN_CQ_CATEGORIES + "] = '%s'" + - " AND node.[" + CommerceExperienceFragment.PN_FRAGMENT_LOCATION + "] %s"; @Rule public final AemContext context = buildAemContext("/context/jcr-content-experiencefragment.json") .afterSetUp(context -> { context.registerService(LiveRelationshipManager.class, mock(LiveRelationshipManager.class)); - LanguageManager languageManager = context.registerService(LanguageManager.class, mock(LanguageManager.class)); - Page rootPage = context.pageManager().getPage(PAGE); - Mockito.when(languageManager.getLanguageRoot(any())).thenReturn(rootPage); + CommerceExperienceFragmentsRetriever cxfRetriever = context.registerService(CommerceExperienceFragmentsRetriever.class, mock( + CommerceExperienceFragmentsRetriever.class)); + + Resource xf1uid = context.resourceResolver().getResource( + "/content/experience-fragments/mysite/page/xf-1-uid/master/jcr:content"); + Resource xf2uid = context.resourceResolver() + .getResource("/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content"); + Mockito.when(cxfRetriever.getExperienceFragmentsForProduct(eq("sku-xf1"), + isNull(String.class), any())).thenReturn(Collections.singletonList(xf1uid)); + Mockito.when(cxfRetriever.getExperienceFragmentsForProduct(eq("sku-xf2"), + eq("location-xf2"), any())).thenReturn(Collections.singletonList(xf2uid)); + Mockito.when(cxfRetriever.getExperienceFragmentsForProduct(eq("sku-xf3"), + any(), any())).thenReturn(Collections.emptyList()); + Mockito.when(cxfRetriever.getExperienceFragmentsForProduct(isNull(String.class), + any(), any())).thenReturn(Collections.emptyList()); + Mockito.when(cxfRetriever.getExperienceFragmentsForCategory(eq("uid1"), + isNull(String.class), any())).thenReturn(Collections.singletonList(xf1uid)); + Mockito.when(cxfRetriever.getExperienceFragmentsForCategory(eq("uid2"), + eq("location-xf2"), any())).thenReturn(Collections.singletonList(xf2uid)); + Mockito.when(cxfRetriever.getExperienceFragmentsForCategory(eq("uid3"), any(), any())).thenReturn(Collections.emptyList()); + Mockito.when(cxfRetriever.getExperienceFragmentsForCategory(isNull(String.class), + any(), any())).thenReturn(Collections.emptyList()); }) .build(); @@ -120,17 +127,6 @@ private void setup(String pagePath, String resourcePath) throws IOException { } - private String buildQuery(String xfRoot, String productSku, String categoryId, String fragmentLocation) { - String flCondition = fragmentLocation != null ? "= '" + fragmentLocation + "'" : "IS NULL"; - String query; - if (productSku != null) { - query = String.format(PRODUCT_QUERY_TEMPLATE, xfRoot, productSku, productSku, flCondition); - } else { - query = String.format(CATEGORY_QUERY_TEMPLATE, xfRoot, categoryId, flCondition); - } - return query; - } - @Test public void testFragmentOnProductPageWithoutLocationProperty() throws IOException { setup(PRODUCT_PAGE, RESOURCE_XF1); @@ -138,7 +134,7 @@ public void testFragmentOnProductPageWithoutLocationProperty() throws IOExceptio MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/url-key-xf1.html"); - verifyFragment(SITE_XF_ROOT, "sku-xf1", null, null, "xf-1-uid", + verifyFragment("sku-xf1", null, null, "xf-1-uid", "/content/experience-fragments/mysite/page/xf-1-uid/master/jcr:content"); } @@ -149,21 +145,10 @@ public void testFragmentOnProductPageWithLocationProperty() throws IOException { MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/url-key-xf2.html"); - verifyFragment(SITE_XF_ROOT, "sku-xf2", null, "location-xf2", "xf-2-uid", + verifyFragment("sku-xf2", null, "location-xf2", "xf-2-uid", "/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content"); } - @Test - public void testFragmentOnProductPageWithInvalidLanguageManager() throws IOException { - Mockito.reset(context.getService(LanguageManager.class)); - setup(PRODUCT_PAGE, RESOURCE_XF1); - - MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); - requestPathInfo.setSuffix("/url-key-xf1.html"); - - verifyFragment(XF_ROOT, "sku-xf1", null, null, "xf-1-uid", "/content/experience-fragments/mysite/page/xf-1-uid/master/jcr:content"); - } - @Test public void testFragmentOnProductPageWithoutMatchingSkus() throws IOException { setup(PRODUCT_PAGE, RESOURCE_XF2); @@ -171,7 +156,7 @@ public void testFragmentOnProductPageWithoutMatchingSkus() throws IOException { MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/sku-xf3.html"); - verifyFragmentResourceIsNull(XF_ROOT, "sku-xf3", null, "location-xf2"); + verifyFragmentResourceIsNull("sku-xf3", null, "location-xf2"); } @Test @@ -190,7 +175,7 @@ public void testFragmentOnNonProductOrCategoryPage() throws IOException { MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSelectorString("sku-xf1"); - verifyFragmentResourceIsNull(XF_ROOT, "sku-xf1", null, null); + verifyFragmentResourceIsNull("sku-xf1", null, null); } @Test @@ -200,7 +185,7 @@ public void testFragmentOnCategoryPageWithoutLocationProperty() throws IOExcepti MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/uid1.html"); - verifyFragment(SITE_XF_ROOT, null, "uid1", null, "xf-1-uid", + verifyFragment(null, "uid1", null, "xf-1-uid", "/content/experience-fragments/mysite/page/xf-1-uid/master/jcr:content"); } @@ -211,7 +196,7 @@ public void testFragmentOnCategoryPageWithLocationProperty() throws IOException MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/uid2.html"); - verifyFragment(SITE_XF_ROOT, null, "uid2", "location-xf2", "xf-2-uid", + verifyFragment(null, "uid2", "location-xf2", "xf-2-uid", "/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content"); } @@ -222,14 +207,14 @@ public void testFragmentOnCategoryPageWithoutMatchingUids() throws IOException { MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/uid3.html"); - verifyFragmentResourceIsNull(XF_ROOT, null, "uid3", "location-xf2"); + verifyFragmentResourceIsNull(null, "uid3", "location-xf2"); } @Test public void testFragmentOnCategoryPageWithInvalidUid() throws IOException { setup(CATEGORY_PAGE, RESOURCE_XF2); - verifyFragmentResourceIsNull(XF_ROOT, null, null, null); + verifyFragmentResourceIsNull(null, null, null); } @Test @@ -239,37 +224,23 @@ public void testUIDSupportWithURLPathSelector() throws IOException { MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); requestPathInfo.setSuffix("/url_path2.html"); - verifyFragment(SITE_XF_ROOT, null, "uid2", "location-xf2", "xf-2-uid", + verifyFragment(null, "uid2", "location-xf2", "xf-2-uid", "/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content"); } - private void verifyFragment(String xfRootPath, String productSku, String categoryId, String fragmentLocation, String expectedXFName, + private void verifyFragment(String productSku, String categoryId, String fragmentLocation, String expectedXFName, String expectedXFPath) { - XFMockQueryResultHandler queryHandler = mockJcrQueryResult(xfRootPath, productSku, categoryId, fragmentLocation); - CommerceExperienceFragmentImpl cxf = context.request().adaptTo(CommerceExperienceFragmentImpl.class); Assert.assertNotNull(cxf); Assert.assertEquals(expectedXFName, cxf.getName()); Assert.assertEquals(CommerceExperienceFragmentImpl.RESOURCE_TYPE, cxf.getExportedType()); Assert.assertEquals(expectedXFPath, cxf.getExperienceFragmentResource().getPath()); - String expectedQuery = buildQuery(xfRootPath, productSku, categoryId, fragmentLocation); - Assert.assertEquals(expectedQuery, queryHandler.getQuery().getStatement()); } - private void verifyFragmentResourceIsNull(String xfRootPath, String productSku, String categoryId, String fragmentLocation) { - mockJcrQueryResult(xfRootPath, productSku, categoryId, fragmentLocation); - + private void verifyFragmentResourceIsNull(String productSku, String categoryId, String fragmentLocation) { CommerceExperienceFragmentImpl cxf = context.request().adaptTo(CommerceExperienceFragmentImpl.class); Assert.assertNotNull(cxf); Assert.assertNull(cxf.getExperienceFragmentResource()); } - - private XFMockQueryResultHandler mockJcrQueryResult(String xfRootPath, String productSku, String categoryId, String fragmentLocation) { - Resource pageResource = context.resourceResolver().getResource(xfRootPath); - Session session = context.resourceResolver().adaptTo(Session.class); - XFMockQueryResultHandler queryHandler = new XFMockQueryResultHandler(pageResource, productSku, categoryId, fragmentLocation); - MockJcr.addQueryResultHandler(session, queryHandler); - return queryHandler; - } } diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/page/PageMetadataImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/page/PageMetadataImplTest.java index 38cc9687e6..869b1bcd28 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/page/PageMetadataImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/page/PageMetadataImplTest.java @@ -39,6 +39,7 @@ import com.adobe.cq.commerce.core.MockHttpClientBuilderFactory; import com.adobe.cq.commerce.core.components.internal.services.CommerceComponentModelFinder; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.models.common.ProductListItem; import com.adobe.cq.commerce.core.components.models.page.PageMetadata; import com.adobe.cq.commerce.core.components.models.product.Product; @@ -94,6 +95,8 @@ public class PageMetadataImplTest { // TODO: CIF-2469 Whitebox.setInternalState(componentModelFinder, "modelFactory", context.getService(ModelFactory.class)); context.registerService(CommerceComponentModelFinder.class, componentModelFinder); + + context.registerService(CommerceExperienceFragmentsRetriever.class, mock(CommerceExperienceFragmentsRetriever.class)); }) .build(); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImplTest.java index 9d5dd0806d..49b4f20b6f 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/productlist/ProductListImplTest.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.lang.reflect.Proxy; import java.text.NumberFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Currency; import java.util.List; @@ -50,9 +51,11 @@ import org.mockito.runners.MockitoJUnitRunner; import com.adobe.cq.commerce.core.MockHttpClientBuilderFactory; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.internal.services.sitemap.SitemapLinkExternalizer; import com.adobe.cq.commerce.core.components.internal.services.sitemap.SitemapLinkExternalizerProvider; import com.adobe.cq.commerce.core.components.models.common.ProductListItem; +import com.adobe.cq.commerce.core.components.models.experiencefragment.CommerceExperienceFragmentContainer; import com.adobe.cq.commerce.core.components.models.retriever.AbstractCategoryRetriever; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.commerce.core.components.services.urls.CategoryUrlFormat; @@ -81,8 +84,10 @@ import com.adobe.cq.commerce.magento.graphql.Query; import com.adobe.cq.commerce.magento.graphql.gson.QueryDeserializer; import com.adobe.cq.sightly.SightlyWCMMode; +import com.day.cq.wcm.api.LanguageManager; import com.day.cq.wcm.api.Page; import com.day.cq.wcm.api.designer.Style; +import com.day.cq.wcm.msm.api.LiveRelationshipManager; import com.day.cq.wcm.scripting.WCMBindingsConstants; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; @@ -93,6 +98,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -102,9 +108,11 @@ @RunWith(MockitoJUnitRunner.class) public class ProductListImplTest { - private static final ValueMap MOCK_CONFIGURATION = new ValueMapDecorator(ImmutableMap.of("cq:graphqlClient", "default", "magentoStore", - "my-store", "enableUIDSupport", "true")); - private static final ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration(MOCK_CONFIGURATION); + private static final ValueMap MOCK_CONFIGURATION = new ValueMapDecorator( + ImmutableMap.of("cq:graphqlClient", "default", "magentoStore", + "my-store", "enableUIDSupport", "true")); + private static final ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration( + MOCK_CONFIGURATION); @Rule public final AemContext context = buildAemContext("/context/jcr-content.json") @@ -115,6 +123,17 @@ public class ProductListImplTest { Utils.addDataLayerConfig(mockConfigBuilder, true); Utils.addStorefrontContextConfig(mockConfigBuilder, true); context.registerAdapter(Resource.class, ConfigurationBuilder.class, mockConfigBuilder); + context.registerService(LiveRelationshipManager.class, mock(LiveRelationshipManager.class)); + LanguageManager languageManager = context.registerService(LanguageManager.class, + mock(LanguageManager.class)); + Page rootPage = context.pageManager().getPage(PAGE); + Mockito.when(languageManager.getLanguageRoot(any())).thenReturn(rootPage); + CommerceExperienceFragmentsRetriever cxfRetriever = context.registerService( + CommerceExperienceFragmentsRetriever.class, + mock(CommerceExperienceFragmentsRetriever.class)); + List xfs = new ArrayList<>(); + xfs.add(context.resourceResolver().getResource("/content/experience-fragments/pageA/xf")); + Mockito.when(cxfRetriever.getExperienceFragmentsForCategory(any(), eq("grid"), any())).thenReturn(xfs); }) .build(); @@ -122,6 +141,8 @@ public class ProductListImplTest { private static final String PAGE = "/content/pageA"; private static final String PRODUCTLIST = "/content/pageA/jcr:content/root/responsivegrid/productlist"; private static final String PRODUCT_LIST_NO_SORTING = "/content/pageA/jcr:content/root/responsivegrid/productlist_no_sorting"; + private static final String PRODUCT_LIST_WITH_XF = "/content/pageA/jcr:content/root/responsivegrid/productlist_with_xf"; + private static final String PRODUCT_LIST_WITH_MULTIPLE_XF = "/content/pageA/jcr:content/root/responsivegrid/productlist_with_multiple_xf"; private Resource productListResource; private Resource pageResource; @@ -141,25 +162,37 @@ public void setUp() throws Exception { context.registerService(HttpClientBuilderFactory.class, new MockHttpClientBuilderFactory(httpClient)); - category = Utils.getQueryFromResource("graphql/magento-graphql-search-category-result-category.json").getCategoryList().get(0); - products = Utils.getQueryFromResource("graphql/magento-graphql-search-category-result-products.json").getProducts(); + category = Utils.getQueryFromResource("graphql/magento-graphql-search-category-result-category.json") + .getCategoryList().get(0); + products = Utils.getQueryFromResource("graphql/magento-graphql-search-category-result-products.json") + .getProducts(); graphqlClient = Mockito.spy(new GraphqlClientImpl()); context.registerInjectActivateService(graphqlClient, "httpMethod", "POST"); - Utils.setupHttpResponse("graphql/magento-graphql-introspection-result.json", httpClient, HttpStatus.SC_OK, "{__type"); - Utils.setupHttpResponse("graphql/magento-graphql-attributes-result.json", httpClient, HttpStatus.SC_OK, "{customAttributeMetadata"); + Utils.setupHttpResponse("graphql/magento-graphql-introspection-result.json", httpClient, HttpStatus.SC_OK, + "{__type"); + Utils.setupHttpResponse("graphql/magento-graphql-attributes-result.json", httpClient, HttpStatus.SC_OK, + "{customAttributeMetadata"); Utils.setupHttpResponse("graphql/magento-graphql-category-uid.json", httpClient, HttpStatus.SC_OK, "{categoryList(filters:{url_path"); - Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-category.json", httpClient, HttpStatus.SC_OK, + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-category.json", httpClient, + HttpStatus.SC_OK, "{categoryList(filters:{category_uid"); - Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products.json", httpClient, HttpStatus.SC_OK, "{products"); + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products.json", httpClient, + HttpStatus.SC_OK, "pageSize:6"); + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products.json", httpClient, + HttpStatus.SC_OK, "pageSize:5"); + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products-pagesize-1.json", httpClient, + HttpStatus.SC_OK, + "pageSize:1"); // magento-graphql-category-uid when(productListResource.adaptTo(ComponentsConfiguration.class)).thenReturn(MOCK_CONFIGURATION_OBJECT); - context.registerAdapter(Resource.class, GraphqlClient.class, (Function) input -> input.getValueMap().get( - "cq:graphqlClient", String.class) != null ? graphqlClient : null); + context.registerAdapter(Resource.class, GraphqlClient.class, + (Function) input -> input.getValueMap().get( + "cq:graphqlClient", String.class) != null ? graphqlClient : null); // This is needed by the SearchResultsService used by the productlist component pageResource = Mockito.spy(page.adaptTo(Resource.class)); @@ -174,7 +207,8 @@ public void setUp() throws Exception { requestPathInfo.setSuffix("/category-1.html"); context.request().setServletPath(PAGE + ".html/category-1.html"); // used by context.request().getRequestURI(); - // This sets the page attribute injected in the models with @Inject or @ScriptVariable + // This sets the page attribute injected in the models with @Inject or + // @ScriptVariable SlingBindings slingBindings = (SlingBindings) context.request().getAttribute(SlingBindings.class.getName()); slingBindings.setResource(productListResource); slingBindings.put(WCMBindingsConstants.NAME_CURRENT_PAGE, page); @@ -214,7 +248,8 @@ protected void testStagedDataImpl(boolean hasStagedData) { Collection products = productListModel.getProducts(); // We cannot differentiate if the items are created from a productlist v1 and v2 - // and do not want to introduce a new mock JSON response for this, so this is always "true" + // and do not want to introduce a new mock JSON response for this, so this is + // always "true" Assert.assertTrue(products.stream().allMatch(p -> p.isStaged().equals(true))); } @@ -289,20 +324,26 @@ public void getProducts() { Assert.assertEquals(productInterface.getName(), item.getTitle()); Assert.assertEquals(productInterface.getSku(), item.getSKU()); Assert.assertEquals(productInterface.getUrlKey(), item.getSlug()); - Assert.assertEquals(String.format(PRODUCT_PAGE + ".html/%s.html", productInterface.getUrlKey()), item.getURL()); + Assert.assertEquals(String.format(PRODUCT_PAGE + ".html/%s.html", productInterface.getUrlKey()), + item.getURL()); Assert.assertEquals(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getValue(), item.getPriceRange().getFinalPrice(), 0); - Assert.assertEquals(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getCurrency().toString(), + Assert.assertEquals( + productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getCurrency().toString(), item.getPriceRange().getCurrency()); - priceFormatter.setCurrency(Currency.getInstance(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getCurrency() - .toString())); - Assert.assertEquals(priceFormatter.format(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getValue()), + priceFormatter.setCurrency(Currency + .getInstance(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getCurrency() + .toString())); + Assert.assertEquals( + priceFormatter + .format(productInterface.getPriceRange().getMinimumPrice().getFinalPrice().getValue()), item.getPriceRange().getFormattedFinalPrice()); ProductImage smallImage = productInterface.getSmallImage(); if (smallImage == null) { - // if small image is missing for a product in GraphQL response then image URL is null for the related item + // if small image is missing for a product in GraphQL response then image URL is + // null for the related item Assert.assertNull(item.getImageURL()); } else { Assert.assertEquals(smallImage.getUrl(), item.getImageURL()); @@ -316,8 +357,9 @@ public void getProducts() { Assert.assertEquals(8, searchAggregations.size()); // check category aggregation - Optional categoryIdAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals( - ProductListImpl.CATEGORY_AGGREGATION_ID)) + Optional categoryIdAggregation = searchAggregations.stream() + .filter(a -> a.getIdentifier().equals( + ProductListImpl.CATEGORY_AGGREGATION_ID)) .findAny(); Assert.assertTrue(categoryIdAggregation.isPresent()); List options = categoryIdAggregation.get().getOptions(); @@ -334,7 +376,8 @@ public void getProducts() { Assert.assertEquals("/content/category-page.html/running/bags.html", opt.getPageUrl()); // We want to make sure all price ranges are properly processed - SearchAggregation priceAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals("price")).findFirst().get(); + SearchAggregation priceAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals("price")) + .findFirst().get(); Assert.assertEquals(3, priceAggregation.getOptions().size()); Assert.assertEquals(3, priceAggregation.getOptionCount()); Assert.assertTrue(priceAggregation.getOptions().stream().anyMatch(o -> o.getDisplayLabel().equals("30-40"))); @@ -342,6 +385,59 @@ public void getProducts() { Assert.assertTrue(priceAggregation.getOptions().stream().anyMatch(o -> o.getDisplayLabel().equals("14"))); } + @Test + public void getProductsWithOneFragment() { + context.currentResource(PRODUCT_LIST_WITH_XF); + adaptToProductList(); + + List fragments = productListModel.getExperienceFragments(); + Assert.assertNotNull(fragments); + Assert.assertEquals(1, fragments.size()); + + CommerceExperienceFragmentContainer xfItem = fragments.get(0); + Assert.assertNotNull(xfItem.getCssClassName()); + Assert.assertNotNull(xfItem.getRenderResource()); + } + + @Test + public void getProductsWithMultipleFragments() { + context.currentResource(PRODUCT_LIST_WITH_MULTIPLE_XF); + adaptToProductList(); + + List fragments = productListModel.getExperienceFragments(); + Assert.assertNotNull(fragments); + + // There are 4 configured XFs. one has a location that does not match any XF + // resource and another one is set for page 2 + Assert.assertEquals(2, fragments.size()); + + CommerceExperienceFragmentContainer xfItem = fragments.get(0); + Assert.assertNull(xfItem.getCssClassName()); + Assert.assertNotNull(xfItem.getRenderResource()); + + xfItem = fragments.get(1); + Assert.assertNotNull(xfItem.getCssClassName()); + Assert.assertNotNull(xfItem.getRenderResource()); + } + + @Test + public void getProductsWithMultipleFragmentsPage2() { + context.currentResource(PRODUCT_LIST_WITH_MULTIPLE_XF); + context.request().getParameterMap().put("page", new String[] { "2" }); + adaptToProductList(); + + List fragments = productListModel.getExperienceFragments(); + Assert.assertNotNull(fragments); + + // There are 4 configured XFs. one has a location that does not match any XF + // resource and another one is set for page 2 + Assert.assertEquals(1, fragments.size()); + + CommerceExperienceFragmentContainer xfItem = fragments.get(0); + Assert.assertNotNull(xfItem.getCssClassName()); + Assert.assertNotNull(xfItem.getRenderResource()); + } + // custom marker interface for search aggregation options private interface MySearchAggregationOption {}; @@ -349,7 +445,8 @@ private interface MySearchAggregationOption {}; public void getProductsWithCustomAggregationOptions() { adaptToProductList(); - // inject custom search results service which returns custom search aggregation objects + // inject custom search results service which returns custom search aggregation + // objects SearchResultsService searchResultsService = (SearchResultsService) Whitebox.getInternalState(productListModel, "searchResultsService"); ClassLoader classLoader = getClass().getClassLoader(); @@ -369,7 +466,8 @@ public void getProductsWithCustomAggregationOptions() { List options = aggregation.getOptions(); List myOptions = options.stream().map( o -> (SearchAggregationOption) Proxy.newProxyInstance(classLoader, optionInterfaces, - (oProxy, oMethod, oArgs) -> oMethod.invoke(o, oArgs))).collect(Collectors.toList()); + (oProxy, oMethod, oArgs) -> oMethod.invoke(o, oArgs))) + .collect(Collectors.toList()); options.clear(); options.addAll(myOptions); } @@ -391,8 +489,9 @@ public void getProductsWithCustomAggregationOptions() { Assert.assertEquals(8, searchAggregations.size()); // check category aggregation - Optional categoryIdAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals( - ProductListImpl.CATEGORY_AGGREGATION_ID)) + Optional categoryIdAggregation = searchAggregations.stream() + .filter(a -> a.getIdentifier().equals( + ProductListImpl.CATEGORY_AGGREGATION_ID)) .findAny(); Assert.assertTrue(categoryIdAggregation.isPresent()); List options = categoryIdAggregation.get().getOptions(); @@ -412,7 +511,8 @@ public void getProductsWithCustomAggregationOptions() { Assert.assertEquals("/content/category-page.html/running/bags.html", opt.getPageUrl()); // We want to make sure all price ranges are properly processed - SearchAggregation priceAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals("price")).findFirst().get(); + SearchAggregation priceAggregation = searchAggregations.stream().filter(a -> a.getIdentifier().equals("price")) + .findFirst().get(); Assert.assertEquals(3, priceAggregation.getOptions().size()); Assert.assertEquals(3, priceAggregation.getOptionCount()); Assert.assertTrue(priceAggregation.getOptions().stream().anyMatch(o -> o.getDisplayLabel().equals("30-40"))); @@ -425,16 +525,20 @@ public void getProductsWithCustomAggregationOptions() { @Test public void testFilterQueriesReturnNull() throws IOException { - // We want to make sure that components will not fail if the __type and/or customAttributeMetadata fields are null + // We want to make sure that components will not fail if the __type and/or + // customAttributeMetadata fields are null // For example, 3rd-party integrations might not support this immediately Mockito.reset(httpClient); Utils.setupHttpResponse("graphql/magento-graphql-empty-data.json", httpClient, HttpStatus.SC_OK, "{__type"); - Utils.setupHttpResponse("graphql/magento-graphql-empty-data.json", httpClient, HttpStatus.SC_OK, "{customAttributeMetadata"); - Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products.json", httpClient, HttpStatus.SC_OK, "{products"); + Utils.setupHttpResponse("graphql/magento-graphql-empty-data.json", httpClient, HttpStatus.SC_OK, + "{customAttributeMetadata"); + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-products.json", httpClient, + HttpStatus.SC_OK, "{products"); Utils.setupHttpResponse("graphql/magento-graphql-category-uid.json", httpClient, HttpStatus.SC_OK, "{categoryList(filters:{url_path"); - Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-category.json", httpClient, HttpStatus.SC_OK, + Utils.setupHttpResponse("graphql/magento-graphql-search-category-result-category.json", httpClient, + HttpStatus.SC_OK, "{categoryList(filters:{category_uid"); adaptToProductList(); @@ -471,7 +575,8 @@ public void testMissingSuffixOnPublish() throws IOException { context.request().setServletPath(PAGE + ".html"); // used by context.request().getRequestURI(); adaptToProductList(); - // Check that we get an empty list of products and the GraphQL client is never called + // Check that we get an empty list of products and the GraphQL client is never + // called Assert.assertTrue(productListModel.getProducts().isEmpty()); Mockito.verify(graphqlClient, never()).execute(any(), any(), any()); Mockito.verify(graphqlClient, never()).execute(any(), any(), any(), any()); @@ -540,14 +645,16 @@ public void testSorting() { Map currentOrderParameters = currentKey.getCurrentOrderParameters(); Assert.assertNotNull(currentOrderParameters); Assert.assertEquals(resultSet.getAppliedQueryParameters().size() + 2, currentOrderParameters.size()); - resultSet.getAppliedQueryParameters().forEach((key, value) -> Assert.assertEquals(value, currentOrderParameters.get(key))); + resultSet.getAppliedQueryParameters() + .forEach((key, value) -> Assert.assertEquals(value, currentOrderParameters.get(key))); Assert.assertEquals("price", currentOrderParameters.get(Sorter.PARAMETER_SORT_KEY)); Assert.assertEquals("asc", currentOrderParameters.get(Sorter.PARAMETER_SORT_ORDER)); Map oppositeOrderParameters = currentKey.getOppositeOrderParameters(); Assert.assertNotNull(oppositeOrderParameters); Assert.assertEquals(resultSet.getAppliedQueryParameters().size() + 2, oppositeOrderParameters.size()); - resultSet.getAppliedQueryParameters().forEach((key, value) -> Assert.assertEquals(value, oppositeOrderParameters.get(key))); + resultSet.getAppliedQueryParameters() + .forEach((key, value) -> Assert.assertEquals(value, oppositeOrderParameters.get(key))); Assert.assertEquals("price", oppositeOrderParameters.get(Sorter.PARAMETER_SORT_KEY)); Assert.assertEquals("desc", oppositeOrderParameters.get(Sorter.PARAMETER_SORT_ORDER)); @@ -583,14 +690,16 @@ public void testDefaultSorting() { Map currentOrderParameters = currentKey.getCurrentOrderParameters(); Assert.assertNotNull(currentOrderParameters); Assert.assertEquals(resultSet.getAppliedQueryParameters().size() + 2, currentOrderParameters.size()); - resultSet.getAppliedQueryParameters().forEach((key, value) -> Assert.assertEquals(value, currentOrderParameters.get(key))); + resultSet.getAppliedQueryParameters() + .forEach((key, value) -> Assert.assertEquals(value, currentOrderParameters.get(key))); Assert.assertEquals("price", currentOrderParameters.get(Sorter.PARAMETER_SORT_KEY)); Assert.assertEquals("asc", currentOrderParameters.get(Sorter.PARAMETER_SORT_ORDER)); Map oppositeOrderParameters = currentKey.getOppositeOrderParameters(); Assert.assertNotNull(oppositeOrderParameters); Assert.assertEquals(resultSet.getAppliedQueryParameters().size() + 2, oppositeOrderParameters.size()); - resultSet.getAppliedQueryParameters().forEach((key, value) -> Assert.assertEquals(value, oppositeOrderParameters.get(key))); + resultSet.getAppliedQueryParameters() + .forEach((key, value) -> Assert.assertEquals(value, oppositeOrderParameters.get(key))); Assert.assertEquals("price", oppositeOrderParameters.get(Sorter.PARAMETER_SORT_KEY)); Assert.assertEquals("desc", oppositeOrderParameters.get(Sorter.PARAMETER_SORT_ORDER)); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverTest.java new file mode 100644 index 0000000000..c731b6f86f --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/CommerceExperienceFragmentsRetrieverTest.java @@ -0,0 +1,165 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.services.experiencefragments; + +import java.io.IOException; +import java.util.List; + +import javax.jcr.Session; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.jcr.MockJcr; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.internal.util.reflection.Whitebox; + +import com.day.cq.wcm.api.LanguageManager; +import com.day.cq.wcm.api.Page; +import com.day.cq.wcm.msm.api.LiveRelationshipManager; +import io.wcm.testing.mock.aem.junit.AemContext; + +import static com.adobe.cq.commerce.core.testing.TestContext.buildAemContext; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +public class CommerceExperienceFragmentsRetrieverTest { + private static final String PAGE = "/content/mysite/page"; + private static final String PRODUCT_PAGE = PAGE + "/product-page"; + private static final String CATEGORY_PAGE = PAGE + "/category-page"; + private static final String RESOURCE_XF1 = "/jcr:content/root/xf-component-1"; + private static final String RESOURCE_XF2 = "/jcr:content/root/xf-component-2"; + private static final String XF_ROOT = "/content/experience-fragments/"; + private static final String SITE_XF_ROOT = XF_ROOT + "mysite/page"; + + private Page page; + + @Rule + public final AemContext context = buildAemContext("/context/jcr-content-experiencefragment.json") + .afterSetUp(context -> { + context.registerService(LiveRelationshipManager.class, mock(LiveRelationshipManager.class)); + LanguageManager languageManager = context.registerService(LanguageManager.class, + mock(LanguageManager.class)); + Page rootPage = context.pageManager().getPage(PAGE); + Mockito.when(languageManager.getLanguageRoot(any())).thenReturn(rootPage); + + CommerceExperienceFragmentsRetriever cxfRetriver = new CommerceExperienceFragmentsRetrieverImpl(); + Whitebox.setInternalState(cxfRetriver, "languageManager", context.getService(LanguageManager.class)); + context.registerService(CommerceExperienceFragmentsRetriever.class, cxfRetriver); + }) + .build(); + + private void setup(String pagePath, String resourcePath) { + page = spy(context.currentPage(PRODUCT_PAGE)); + Resource xfResource = context.resourceResolver().getResource(PRODUCT_PAGE + RESOURCE_XF2); + context.currentResource(xfResource); + } + + @Test + public void testFragmentOnProductPageWithoutLocationProperty() throws IOException { + setup(PRODUCT_PAGE, RESOURCE_XF1); + List xfs = getProductFragments(SITE_XF_ROOT, "sku-xf1", null); + + assertNotNull(xfs); + assertFalse("Fragments list empty", xfs.isEmpty()); + assertEquals("/content/experience-fragments/mysite/page/xf-1-uid/master/jcr:content", xfs.get(0).getPath()); + } + + @Test + public void testFragmentOnProductPageWithLocationProperty() throws IOException { + setup(PRODUCT_PAGE, RESOURCE_XF2); + List xfs = getProductFragments(SITE_XF_ROOT, "sku-xf2", "location-xf2"); + + assertNotNull(xfs); + assertFalse("Fragments list empty", xfs.isEmpty()); + assertEquals("/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content", xfs.get(0).getPath()); + } + + @Test + public void testFragmentOnProductPageWithoutMatchingSkus() throws IOException { + setup(PRODUCT_PAGE, RESOURCE_XF2); + List xfs = getProductFragments(SITE_XF_ROOT, "sku-xf3", "location-xf2"); + + assertNotNull(xfs); + assertTrue(xfs.isEmpty()); + } + + @Test + public void testFragmentOnProductPageWithNullSku() throws IOException { + setup(CATEGORY_PAGE, RESOURCE_XF2); + List xfs = getProductFragments(XF_ROOT, null, "location-xf2"); + + assertNotNull(xfs); + assertTrue(xfs.isEmpty()); + } + + @Test + public void testFragmentOnCategoryPageWithLocationProperty() throws IOException { + setup(CATEGORY_PAGE, RESOURCE_XF2); + List xfs = getCategoryFragments(SITE_XF_ROOT, "uid2", "location-xf2"); + + assertNotNull(xfs); + assertFalse("Fragments list empty", xfs.isEmpty()); + assertEquals("/content/experience-fragments/mysite/page/xf-2-uid/master/jcr:content", xfs.get(0).getPath()); + } + + @Test + public void testFragmentOnCategoryPageWithoutMatchingUids() throws IOException { + setup(CATEGORY_PAGE, RESOURCE_XF2); + List xfs = getCategoryFragments(XF_ROOT, "uid3", "location-xf2"); + + assertNotNull(xfs); + assertTrue(xfs.isEmpty()); + } + + @Test + public void testFragmentOnCategoryPageWithNullUid() throws IOException { + setup(CATEGORY_PAGE, RESOURCE_XF2); + List xfs = getCategoryFragments(XF_ROOT, null, "location-xf2"); + + assertNotNull(xfs); + assertTrue(xfs.isEmpty()); + } + + private List getProductFragments(String xfRootPath, String productSku, String fragmentLocation) { + mockJcrQueryResult(xfRootPath, productSku, null, fragmentLocation); + CommerceExperienceFragmentsRetriever cxfRetriever = context + .getService(CommerceExperienceFragmentsRetriever.class); + return cxfRetriever.getExperienceFragmentsForProduct(productSku, fragmentLocation, page); + } + + private List getCategoryFragments(String xfRootPath, String categoryUid, String fragmentLocation) { + mockJcrQueryResult(xfRootPath, null, categoryUid, fragmentLocation); + CommerceExperienceFragmentsRetriever cxfRetriever = context + .getService(CommerceExperienceFragmentsRetriever.class); + return cxfRetriever.getExperienceFragmentsForCategory(categoryUid, fragmentLocation, page); + } + + private XFMockQueryResultHandler mockJcrQueryResult(String xfRootPath, String productSku, String categoryId, + String fragmentLocation) { + Resource pageResource = context.resourceResolver().getResource(xfRootPath); + Session session = context.resourceResolver().adaptTo(Session.class); + XFMockQueryResultHandler queryHandler = new XFMockQueryResultHandler(pageResource, productSku, categoryId, + fragmentLocation); + MockJcr.addQueryResultHandler(session, queryHandler); + return queryHandler; + } +} diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/XFMockQueryResultHandler.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/XFMockQueryResultHandler.java similarity index 91% rename from bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/XFMockQueryResultHandler.java rename to bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/XFMockQueryResultHandler.java index f0d3a8dac7..9700e5c19d 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/experiencefragment/XFMockQueryResultHandler.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/experiencefragments/XFMockQueryResultHandler.java @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -package com.adobe.cq.commerce.core.components.internal.models.v1.experiencefragment; +package com.adobe.cq.commerce.core.components.internal.services.experiencefragments; import java.util.ArrayList; import java.util.Iterator; @@ -43,13 +43,18 @@ public class XFMockQueryResultHandler implements MockQueryResultHandler { private MockQuery query; /** - * Instantiates a result handler that will start looking at the root resource, - * and will look for resources matching the given sku and fragmentLocation parameters. - * + * Instantiates a result handler that will start looking at the + * root resource, + * and will look for resources matching the given sku and + * fragmentLocation parameters. + * * @param root The resource where the search should start. - * @param productSku The value of the cq:products property, can be null. - * @param categoryId The value of the cq:categories property, can be null. - * @param fragmentLocation The value of the fragmentLocation property, can be null. + * @param productSku The value of the cq:products property, + * can be null. + * @param categoryId The value of the cq:categories property, + * can be null. + * @param fragmentLocation The value of the fragmentLocation + * property, can be null. */ XFMockQueryResultHandler(Resource root, String productSku, String categoryId, String fragmentLocation) { this.root = root; diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/CatalogPageNotFoundFilterTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/CatalogPageNotFoundFilterTest.java index deca34c150..3ff5358b26 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/CatalogPageNotFoundFilterTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/CatalogPageNotFoundFilterTest.java @@ -41,6 +41,7 @@ import com.adobe.cq.commerce.core.MockHttpClientBuilderFactory; import com.adobe.cq.commerce.core.components.internal.services.CommerceComponentModelFinder; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.search.internal.services.SearchFilterServiceImpl; import com.adobe.cq.commerce.core.search.internal.services.SearchResultsServiceImpl; import com.adobe.cq.commerce.core.testing.TestContext; @@ -88,6 +89,9 @@ public void setup() throws IOException { this.contentModelFinder = spy(commerceModelFinder); aemContext.registerService(CommerceComponentModelFinder.class, this.contentModelFinder); + aemContext.registerService(CommerceExperienceFragmentsRetriever.class, + mock(CommerceExperienceFragmentsRetriever.class)); + aemContext.registerInjectActivateService(new SearchFilterServiceImpl()); aemContext.registerInjectActivateService(new SearchResultsServiceImpl()); aemContext.registerInjectActivateService(subject); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServletTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServletTest.java new file mode 100644 index 0000000000..463693b782 --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesDataSourceServletTest.java @@ -0,0 +1,131 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.servlets; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.adobe.granite.ui.components.ds.DataSource; +import com.day.cq.wcm.api.policies.ContentPolicy; +import com.day.cq.wcm.api.policies.ContentPolicyManager; +import io.wcm.testing.mock.aem.junit.AemContext; +import io.wcm.testing.mock.aem.junit.AemContextBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class ProductListXfStylesDataSourceServletTest { + + private ProductListXfStylesDataSourceServlet dataSourceServlet; + + @Rule + public final AemContext context = new AemContextBuilder(ResourceResolverType.JCR_MOCK).build(); + + ContentPolicyManager policyManager; + ContentPolicy policy; + SlingHttpServletRequest request; + + @Before + public void setUp() { + dataSourceServlet = new ProductListXfStylesDataSourceServlet(); + context.load().json("/context/jcr-conf.json", "/conf/testing"); + context.load().json("/context/jcr-content.json", "/content"); + + request = spy(context.request()); + RequestPathInfo requestPathInfo = mock(RequestPathInfo.class); + when(request.getRequestPathInfo()).thenReturn(requestPathInfo); + when(requestPathInfo.getSuffixResource()) + .thenReturn(context.resourceResolver().getResource("/content/pageA/jcr:content/root/responsivegrid/productlist_with_xf")); + + policyManager = mock(ContentPolicyManager.class); + policy = mock(ContentPolicy.class); + + context.registerAdapter(ResourceResolver.class, ContentPolicyManager.class, policyManager); + } + + @Test + public void testDataSource() throws ServletException, IOException { + when(policy.getPath()).thenReturn("/conf/testing/settings/wcm/policies/testing"); + when(policyManager.getPolicy((Resource) any())).thenReturn(policy); + // Call datasource servlet + dataSourceServlet.doGet(request, context.response()); + DataSource dataSource = (DataSource) context.request().getAttribute(DataSource.class.getName()); + assertNotNull(dataSource); + + List styles = new ArrayList<>(); + dataSource.iterator().forEachRemaining(resource -> { + styles.add((ProductListXfStylesDataSourceServlet.XfStyleResource) resource); + }); + + // Verify values + assertEquals(1, styles.size()); + ProductListXfStylesDataSourceServlet.XfStyleResource first = styles.get(0); + assertEquals("marketing-content__row-2", first.getValue()); + assertEquals("Second Row", first.getText()); + } + + @Test + public void testDataSourceWithNoPolicy() throws ServletException, IOException { + when(policyManager.getPolicy((Resource) any())).thenReturn(null); + // Call datasource servlet + dataSourceServlet.doGet(request, context.response()); + DataSource dataSource = (DataSource) context.request().getAttribute(DataSource.class.getName()); + assertNotNull(dataSource); + + List styles = new ArrayList<>(); + dataSource.iterator().forEachRemaining(resource -> { + styles.add((ProductListXfStylesDataSourceServlet.XfStyleResource) resource); + }); + + // Verify values + assertEquals(0, styles.size()); + } + + @Test + public void testDataSourceWithNoValues() throws ServletException, IOException { + when(policy.getPath()).thenReturn("/conf/testing/settings/wcm/policies/empty"); + when(policyManager.getPolicy((Resource) any())).thenReturn(policy); + + // Call datasource servlet + dataSourceServlet.doGet(request, context.response()); + DataSource dataSource = (DataSource) context.request().getAttribute(DataSource.class.getName()); + assertNotNull(dataSource); + + List styles = new ArrayList<>(); + dataSource.iterator().forEachRemaining(resource -> { + styles.add((ProductListXfStylesDataSourceServlet.XfStyleResource) resource); + }); + + // Verify values + assertEquals(0, styles.size()); + } +} diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServletTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServletTest.java new file mode 100644 index 0000000000..32da3ab4cc --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/servlets/ProductListXfStylesRenderConditionServletTest.java @@ -0,0 +1,110 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2022 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.servlets; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.testing.mock.sling.ResourceResolverType; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.adobe.granite.ui.components.rendercondition.RenderCondition; +import com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition; +import com.day.cq.wcm.api.policies.ContentPolicy; +import com.day.cq.wcm.api.policies.ContentPolicyManager; +import io.wcm.testing.mock.aem.junit.AemContext; +import io.wcm.testing.mock.aem.junit.AemContextBuilder; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class ProductListXfStylesRenderConditionServletTest { + + private ProductListXfStylesRenderConditionServlet renderConditionServlet; + + @Rule + public final AemContext context = new AemContextBuilder(ResourceResolverType.JCR_MOCK).build(); + + ContentPolicyManager policyManager; + ContentPolicy policy; + SlingHttpServletRequest request; + + @Before + public void setUp() { + renderConditionServlet = new ProductListXfStylesRenderConditionServlet(); + context.load().json("/context/jcr-conf.json", "/conf/testing"); + context.load().json("/context/jcr-content.json", "/content"); + + request = spy(context.request()); + RequestPathInfo requestPathInfo = mock(RequestPathInfo.class); + when(request.getRequestPathInfo()).thenReturn(requestPathInfo); + when(requestPathInfo.getSuffixResource()) + .thenReturn(context.resourceResolver().getResource("/content/pageA/jcr:content/root/responsivegrid/productlist_with_xf")); + + policyManager = mock(ContentPolicyManager.class); + policy = mock(ContentPolicy.class); + + context.registerAdapter(ResourceResolver.class, ContentPolicyManager.class, policyManager); + } + + @Test + public void testDataSource() throws ServletException, IOException { + when(policy.getPath()).thenReturn("/conf/testing/settings/wcm/policies/testing"); + when(policyManager.getPolicy((Resource) any())).thenReturn(policy); + // Call datasource servlet + renderConditionServlet.doGet(request, context.response()); + SimpleRenderCondition condition = (SimpleRenderCondition) context.request().getAttribute(RenderCondition.class.getName()); + assertNotNull(condition); + + assertTrue(condition.check()); + } + + @Test + public void testDataSourceWithNoPolicy() throws ServletException, IOException { + when(policyManager.getPolicy((Resource) any())).thenReturn(null); + // Call datasource servlet + renderConditionServlet.doGet(request, context.response()); + SimpleRenderCondition condition = (SimpleRenderCondition) context.request().getAttribute(RenderCondition.class.getName()); + assertNotNull(condition); + + assertFalse(condition.check()); + } + + @Test + public void testDataSourceWithNoValues() throws ServletException, IOException { + when(policy.getPath()).thenReturn("/conf/testing/settings/wcm/policies/empty"); + when(policyManager.getPolicy((Resource) any())).thenReturn(policy); + + // Call datasource servlet + renderConditionServlet.doGet(request, context.response()); + SimpleRenderCondition condition = (SimpleRenderCondition) context.request().getAttribute(RenderCondition.class.getName()); + assertNotNull(condition); + + assertFalse(condition.check()); + } +} diff --git a/bundles/core/src/test/resources/context/jcr-conf.json b/bundles/core/src/test/resources/context/jcr-conf.json index 7dc8bff185..327d376546 100644 --- a/bundles/core/src/test/resources/context/jcr-conf.json +++ b/bundles/core/src/test/resources/context/jcr-conf.json @@ -3,17 +3,37 @@ "jcr:primaryType": "sling:Folder", "cloudconfigs": { "jcr:primaryType": "sling:Folder", - "commerce": { + "commerce": { + "jcr:primaryType": "cq:Page", + "jcr:mixinTypes": [ + "rep:AccessControllable" + ], + "cq:graphqlClient": "default", + "magentoStore": "my-store", + "aTotallyUnrelatedProperty": "true", + "jcr:createdBy": "admin", + "jcr:created": "Thu Jan 16 2020 12:20:34 GMT+0200" + } + }, + "wcm": { "jcr:primaryType": "cq:Page", - "jcr:mixinTypes": [ - "rep:AccessControllable" - ], - "cq:graphqlClient": "default", - "magentoStore": "my-store", - "aTotallyUnrelatedProperty": "true", - "jcr:createdBy": "admin", - "jcr:created": "Thu Jan 16 2020 12:20:34 GMT+0200" - } + "policies": { + "jcr:primaryType": "cq:Page", + "testing": { + "jcr:primaryType": "nt:unstructured", + "fragmentStyles": { + "jcr:primaryType": "nt:unstructured", + "item0": { + "jcr:primaryType": "nt:unstructured", + "fragmentStyleCssClass": "marketing-content__row-2", + "fragmentStyleName": "Second Row" + } + } + }, + "empty": { + "jcr:primaryType": "nt:unstructured" + } + } } } } \ No newline at end of file diff --git a/bundles/core/src/test/resources/context/jcr-content.json b/bundles/core/src/test/resources/context/jcr-content.json index bb5fe9ae9a..0f07523ba5 100644 --- a/bundles/core/src/test/resources/context/jcr-content.json +++ b/bundles/core/src/test/resources/context/jcr-content.json @@ -148,6 +148,51 @@ "showTitle": true, "sling:resourceType": "venia/components/commerce/productlist" }, + "productlist_with_xf": { + "loadClientPrice": true, + "showImage": true, + "showTitle": true, + "defaultSortField": "price", + "defaultSortOrder": "ASC", + "sling:resourceType": "venia/components/commerce/productlist", + "fragmentEnabled": true, + "fragments": { + "item0": { + "fragmentPage": 1, + "fragmentLocation": "grid", + "fragmentCssClass": "test-class" + } + } + }, + "productlist_with_multiple_xf": { + "loadClientPrice": true, + "showImage": true, + "showTitle": true, + "defaultSortField": "price", + "defaultSortOrder": "ASC", + "sling:resourceType": "venia/components/commerce/productlist", + "fragmentEnabled": true, + "fragments": { + "item0": { + "fragmentPage": 1, + "fragmentLocation": "grid" + }, + "item1": { + "fragmentPage": 1, + "fragmentLocation": "grid", + "fragmentCssClass": "test-class" + }, + "item2": { + "fragmentPage": 1, + "fragmentLocation": "inexistent" + }, + "item3": { + "fragmentPage": 2, + "fragmentLocation": "grid", + "fragmentCssClass": "test-class2" + } + } + }, "searchresults": { "sling:resourceType": "core/cif/components/commerce/searchresults/v2/searchresults", "defaultSortField": "relevance", @@ -620,5 +665,30 @@ } } } + }, + "experience-fragments": { + "jcr:primaryType": "sling:Folder", + "pageA": { + "xf": { + "jcr:primaryType": "cq:Page", + "jcr:content": { + "jcr:primaryType": "cq:PageContent", + "jcr:title": "Test XF", + "sling:resourceType": "cq/experience-fragments/components/experiencefragment" + }, + "master": { + "jcr:primaryType": "cq:Page", + "jcr:content": { + "jcr:primaryType": "cq:PageContent", + "jcr:title": "Text XF Master", + "cq:xfVariantType": "web", + "cq:xfMasterVariation": "true", + "cq:products": "sku-xf2", + "cq:categories": "MTI==", + "fragmentLocation": "grid" + } + } + } + } } -} +} \ No newline at end of file diff --git a/bundles/core/src/test/resources/graphql/magento-graphql-search-category-result-products-pagesize-1.json b/bundles/core/src/test/resources/graphql/magento-graphql-search-category-result-products-pagesize-1.json new file mode 100644 index 0000000000..ff756df81a --- /dev/null +++ b/bundles/core/src/test/resources/graphql/magento-graphql-search-category-result-products-pagesize-1.json @@ -0,0 +1,257 @@ +{ + "data": { + "products": { + "total_count": 4, + "items": [ + { + "__typename": "SimpleProduct", + "uid": "MzQ3OA==", + "sku": "24-WB01", + "staged": true, + "name": "Voyage Yoga Bag", + "small_image": { + "url": "https://some-hostname.magentosite.cloud/pub/media/catalog/product/cache/cafecb2c660d511e3918d429acdc5ff6/w/b/wb01-black-0.jpg" + }, + "url_key": "voyage-yoga-bag", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 32, + "currency": "USD" + }, + "final_price": { + "value": 32, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + } + ], + "aggregations": [ + { + "options": [ + { + "count": 2, + "label": "30-40", + "value": "30_40" + }, + { + "count": 1, + "label": "40-*", + "value": "40_*" + }, + { + "count": 1, + "label": "14", + "value": "14" + } + ], + "attribute_code": "price", + "count": 3, + "label": "Price" + }, + { + "options": [ + { + "count": 3, + "label": "Gear", + "value": "3" + }, + { + "count": 3, + "label": "Bags", + "value": "4" + }, + { + "count": 1, + "label": "Collections", + "value": "7" + } + ], + "attribute_code": "category_id", + "count": 3, + "label": "Category" + }, + { + "options": [ + { + "count": 3, + "label": "Yoga", + "value": "8" + }, + { + "count": 2, + "label": "Gym", + "value": "11" + }, + { + "count": 2, + "label": "School", + "value": "20" + }, + { + "count": 1, + "label": "Urban", + "value": "23" + } + ], + "attribute_code": "activity", + "count": 4, + "label": "Activity" + }, + { + "options": [ + { + "count": 1, + "label": "Messenger", + "value": "27" + }, + { + "count": 1, + "label": "Laptop", + "value": "28" + }, + { + "count": 2, + "label": "Exercise", + "value": "29" + }, + { + "count": 2, + "label": "Tote", + "value": "30" + } + ], + "attribute_code": "style_bags", + "count": 4, + "label": "Style Bags" + }, + { + "options": [ + { + "count": 3, + "label": "Nylon", + "value": "37" + }, + { + "count": 3, + "label": "Polyester", + "value": "38" + }, + { + "count": 1, + "label": "Rayon", + "value": "39" + } + ], + "attribute_code": "material", + "count": 3, + "label": "Material" + }, + { + "options": [ + { + "count": 2, + "label": "Adjustable", + "value": "61" + }, + { + "count": 1, + "label": "Cross Body", + "value": "62" + }, + { + "count": 1, + "label": "Detachable", + "value": "63" + }, + { + "count": 2, + "label": "Double", + "value": "64" + }, + { + "count": 1, + "label": "Padded", + "value": "65" + }, + { + "count": 3, + "label": "Shoulder", + "value": "66" + }, + { + "count": 1, + "label": "Single", + "value": "67" + } + ], + "attribute_code": "strap_bags", + "count": 7, + "label": "Strap/Handle" + }, + { + "options": [ + { + "count": 3, + "label": "Waterproof", + "value": "74" + }, + { + "count": 2, + "label": "Lightweight", + "value": "75" + }, + { + "count": 2, + "label": "Reflective", + "value": "77" + }, + { + "count": 1, + "label": "Laptop Sleeve", + "value": "78" + }, + { + "count": 2, + "label": "Lockable", + "value": "79" + } + ], + "attribute_code": "features_bags", + "count": 5, + "label": "Features" + }, + { + "options": [ + { + "count": 1, + "label": "1", + "value": "1" + } + ], + "attribute_code": "performance_fabric", + "count": 1, + "label": "Performance Fabric" + } + ], + "sort_fields": { + "default": "price", + "options": [ + { + "value": "name", + "label": "Product Name" + }, + { + "value": "price", + "label": "Price" + } + ] + } + } + } +} \ No newline at end of file diff --git a/examples/bundle/src/main/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServlet.java b/examples/bundle/src/main/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServlet.java index b2c9cca99c..d526c825f1 100644 --- a/examples/bundle/src/main/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServlet.java +++ b/examples/bundle/src/main/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServlet.java @@ -96,6 +96,7 @@ public class GraphqlServlet extends SlingAllMethodsServlet { private static final String CATEGORY_UID = "uid-1"; private static final String CATEGORY_STAGED_PRODUCTS_UID = "uid-2"; private static final String CATEGORY_PRODUCT_CAROUSEL_UID = "uid-3"; + private static final String CATEGORY_PRODUCT_LIST_XF_UID = "uid-4"; private static final String STAGED_PRODUCT_URL_KEY = "chaz-crocodile-hoodie"; private static final String STAGED_PRODUCT_SKU = "MH02"; @@ -117,6 +118,7 @@ public class GraphqlServlet extends SlingAllMethodsServlet { private static final String PRODUCT_CAROUSEL_CATEGORY_JSON = "magento-graphql-productcarousel-category.json"; private static final String PRODUCT_TEASER_JSON = "magento-graphql-productteaser.json"; private static final String PRODUCTS_COLLECTION_JSON = "magento-graphql-products-collection.json"; + private static final String PRODUCTS_COLLECTION_XF_JSON = "magento-graphql-products-collection-with-xf.json"; private static final String PRODUCTS_COLLECTION_WITH_STAGED_PRODUCTS_JSON = "magento-graphql-products-collection-with-staged-products.json"; private static final String GROUPED_PRODUCT_JSON = "magento-graphql-grouped-product.json"; private static final String PRODUCTS_JSON = "magento-graphql-products.json"; @@ -124,6 +126,7 @@ public class GraphqlServlet extends SlingAllMethodsServlet { private static final String CATEGORY_UID_JSON = "magento-graphql-category.json"; private static final String CATEGORY_JSON = "magento-graphql-category.json"; private static final String CATEGORY_WITH_STAGED_PRODUCTS_JSON = "magento-graphql-category-with-staged-products.json"; + private static final String CATEGORY_WITH_XF_JSON = "magento-graphql-category-with-xf.json"; private static final String UNKNOWN_CATEGORY_JSON = "magento-graphql-category-empty.json"; private static final String FEATURED_CATEGORY_LIST_JSON = "magento-graphql-featuredcategorylist.json"; private static final String CATEGORIES_CAROUSEL_JSON = "magento-graphql-categories-carousel.json"; @@ -426,6 +429,8 @@ private Products readProductsResponse(DataFetchingEnvironment env) { return readProductsFrom(PRODUCTS_COLLECTION_JSON); } else if (CATEGORY_STAGED_PRODUCTS_UID.equals(uidPattern.group(1))) { return readProductsFrom(PRODUCTS_COLLECTION_WITH_STAGED_PRODUCTS_JSON); + } else if (CATEGORY_PRODUCT_LIST_XF_UID.equals(uidPattern.group(1))) { + return readProductsFrom(PRODUCTS_COLLECTION_XF_JSON); } } else if (urlKeyEqPattern.matches()) { if (GROUPED_PRODUCT_URL_KEY.equals(urlKeyEqPattern.group(1))) { @@ -491,6 +496,9 @@ private List readCategoryListResponse(DataFetchingEnvironment env) } else if (filters.get("url_key").get("eq").equals("unknown-category")) { // return empty response graphqlResponse = readGraphqlResponse(UNKNOWN_CATEGORY_JSON); + } else if (filters.get("url_key").get("eq").equals("outdoor-xf")) { + // The URLProvider example will return category uid + graphqlResponse = readGraphqlResponse(CATEGORY_WITH_XF_JSON); } else { graphqlResponse = readGraphqlResponse(CATEGORY_UID_JSON); } @@ -504,6 +512,8 @@ private List readCategoryListResponse(DataFetchingEnvironment env) graphqlResponse = readGraphqlResponse(CATEGORY_JSON); } else if (filters.get("category_uid").get("eq").equals("uid-2")) { graphqlResponse = readGraphqlResponse(CATEGORY_WITH_STAGED_PRODUCTS_JSON); + } else if (filters.get("category_uid").get("eq").equals("uid-4")) { + graphqlResponse = readGraphqlResponse(CATEGORY_WITH_XF_JSON); } else if (filters.get("category_uid").get("eq").equals("Mg==")) { // The navigation example will require item "Mg==" as the default root category graphqlResponse = readGraphqlResponse(CATEGORY_LIST_TREE_JSON); diff --git a/examples/bundle/src/main/resources/graphql/magento-graphql-category-with-xf.json b/examples/bundle/src/main/resources/graphql/magento-graphql-category-with-xf.json new file mode 100644 index 0000000000..b84924fce1 --- /dev/null +++ b/examples/bundle/src/main/resources/graphql/magento-graphql-category-with-xf.json @@ -0,0 +1,19 @@ +{ + "data": { + "categoryList": [ + { + "uid": "uid-4", + "url_key": "outdoor", + "url_path": "outdoor", + "description": "A collection of products for outdoor activities", + "name": "Outdoor Collection", + "image": null, + "product_count": 0, + "meta_description": "Meta description for Outdoor Collection", + "meta_keywords": "Meta keywords for Outdoor Collection", + "meta_title": "Meta title for Outdoor Collection", + "staged": false + } + ] + } +} diff --git a/examples/bundle/src/main/resources/graphql/magento-graphql-products-collection-with-xf.json b/examples/bundle/src/main/resources/graphql/magento-graphql-products-collection-with-xf.json new file mode 100644 index 0000000000..0c8eaac044 --- /dev/null +++ b/examples/bundle/src/main/resources/graphql/magento-graphql-products-collection-with-xf.json @@ -0,0 +1,490 @@ +{ + "data": { + "products": { + "total_count": 12, + "items": [ + { + "__typename": "SimpleProduct", + "staged": false, + "sku": "24-MB02", + "name": "Fusion Backpack", + "small_image": { + "url": "/content/dam/core-components-examples/library/cif-sample-assets/catalog/product/fusion-backpack/mb02-gray-0.jpg" + }, + "url_key": "fusion-backpack", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 59, + "currency": "USD" + }, + "final_price": { + "value": 59, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + }, + { + "__typename": "SimpleProduct", + "staged": false, + "sku": "24-MG03", + "name": "Summit Watch", + "small_image": { + "url": "/content/dam/core-components-examples/library/cif-sample-assets/catalog/product/summit-watch/mg03-br-0.jpg" + }, + "url_key": "summit-watch", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 54, + "currency": "USD" + }, + "final_price": { + "value": 54, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + }, + { + "__typename": "SimpleProduct", + "staged": false, + "sku": "24-WG01", + "name": "Bolo Sport Watch", + "small_image": { + "url": "/content/dam/core-components-examples/library/cif-sample-assets/catalog/product/bolo-sport-watch/wg01-bk-0.jpg" + }, + "url_key": "bolo-sport-watch", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 49, + "currency": "USD" + }, + "final_price": { + "value": 49, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + }, + { + "__typename": "ConfigurableProduct", + "staged": false, + "sku": "MH01", + "name": "Chaz Kangeroo Hoodie", + "small_image": { + "url": "/content/dam/core-components-examples/library/cif-sample-assets/catalog/product/chaz-kangeroo-hoodie/mh01-gray_main_2.jpg" + }, + "url_key": "chaz-kangeroo-hoodie", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 52, + "currency": "USD" + }, + "final_price": { + "value": 52, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + }, + "maximum_price": { + "regular_price": { + "value": 52, + "currency": "USD" + }, + "final_price": { + "value": 52, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + }, + { + "__typename": "ConfigurableProduct", + "staged": false, + "sku": "MH03", + "name": "Bruno Compete Hoodie", + "small_image": { + "url": "/content/dam/core-components-examples/library/cif-sample-assets/catalog/product/bruno-compete-hoodie/mh03-black_main_2.jpg" + }, + "url_key": "bruno-compete-hoodie", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 63, + "currency": "USD" + }, + "final_price": { + "value": 63, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + }, + "maximum_price": { + "regular_price": { + "value": 63, + "currency": "USD" + }, + "final_price": { + "value": 63, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + } + ], + "aggregations": [ + { + "options": [ + { + "count": 1, + "label": "40-50", + "value": "40_50" + }, + { + "count": 3, + "label": "50-60", + "value": "50_60" + }, + { + "count": 1, + "label": "60-70", + "value": "60_70" + }, + { + "count": 1, + "label": "80-*", + "value": "80_*" + } + ], + "attribute_code": "price", + "count": 4, + "label": "Price" + }, + { + "options": [ + { + "count": 3, + "label": "Gear", + "value": "3" + }, + { + "count": 1, + "label": "Bags", + "value": "4" + }, + { + "count": 2, + "label": "Watches", + "value": "6" + }, + { + "count": 1, + "label": "Collections", + "value": "7" + }, + { + "count": 2, + "label": "New Luma Yoga Collection", + "value": "8" + }, + { + "count": 2, + "label": "Tops", + "value": "13" + }, + { + "count": 2, + "label": "Hoodies & Sweatshirts", + "value": "16" + }, + { + "count": 1, + "label": "Tops", + "value": "22" + }, + { + "count": 1, + "label": "Jackets", + "value": "24" + }, + { + "count": 1, + "label": "Performance Fabrics", + "value": "36" + }, + { + "count": 2, + "label": "Eco Friendly", + "value": "37" + } + ], + "attribute_code": "category_id", + "count": 11, + "label": "Category" + }, + { + "options": [ + { + "count": 4, + "label": "Black", + "value": "49" + }, + { + "count": 1, + "label": "Blue", + "value": "50" + }, + { + "count": 1, + "label": "Gray", + "value": "52" + }, + { + "count": 1, + "label": "Green", + "value": "53" + }, + { + "count": 2, + "label": "Orange", + "value": "56" + }, + { + "count": 1, + "label": "Red", + "value": "58" + }, + { + "count": 1, + "label": "White", + "value": "59" + } + ], + "attribute_code": "color", + "count": 7, + "label": "Color" + }, + { + "options": [ + { + "count": 1, + "label": "Outdoor", + "value": "5" + }, + { + "count": 1, + "label": "Yoga", + "value": "8" + }, + { + "count": 2, + "label": "Recreation", + "value": "9" + }, + { + "count": 1, + "label": "Gym", + "value": "11" + }, + { + "count": 1, + "label": "Athletic", + "value": "16" + }, + { + "count": 2, + "label": "Sports", + "value": "17" + }, + { + "count": 1, + "label": "Hiking", + "value": "18" + }, + { + "count": 1, + "label": "School", + "value": "20" + } + ], + "attribute_code": "activity", + "count": 8, + "label": "Activity" + }, + { + "options": [ + { + "count": 1, + "label": "Burlap", + "value": "31" + }, + { + "count": 2, + "label": "Nylon", + "value": "37" + }, + { + "count": 2, + "label": "Polyester", + "value": "38" + }, + { + "count": 1, + "label": "Metal", + "value": "43" + }, + { + "count": 1, + "label": "Plastic", + "value": "44" + }, + { + "count": 2, + "label": "Silicone", + "value": "48" + }, + { + "count": 1, + "label": "Organic Cotton", + "value": "156" + }, + { + "count": 1, + "label": "CoolTech™", + "value": "158" + }, + { + "count": 1, + "label": "Wool", + "value": "161" + } + ], + "attribute_code": "material", + "count": 9, + "label": "Material" + }, + { + "options": [ + { + "count": 2, + "label": "Electronic", + "value": "86" + }, + { + "count": 2, + "label": "Exercise", + "value": "87" + }, + { + "count": 2, + "label": "Timepiece", + "value": "90" + } + ], + "attribute_code": "category_gear", + "count": 3, + "label": "Category Gear" + }, + { + "options": [ + { + "count": 3, + "label": "XS", + "value": "169" + }, + { + "count": 3, + "label": "S", + "value": "170" + }, + { + "count": 3, + "label": "M", + "value": "171" + }, + { + "count": 3, + "label": "L", + "value": "172" + }, + { + "count": 3, + "label": "XL", + "value": "173" + } + ], + "attribute_code": "size", + "count": 5, + "label": "Size" + }, + { + "options": [ + { + "count": 2, + "label": "All-Weather", + "value": "204" + }, + { + "count": 3, + "label": "Cool", + "value": "206" + }, + { + "count": 2, + "label": "Indoor", + "value": "207" + }, + { + "count": 3, + "label": "Spring", + "value": "210" + }, + { + "count": 3, + "label": "Windy", + "value": "212" + }, + { + "count": 1, + "label": "Wintry", + "value": "213" + } + ], + "attribute_code": "climate", + "count": 6, + "label": "Climate" + } + ] + } + } +} \ No newline at end of file diff --git a/examples/bundle/src/test/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServletTest.java b/examples/bundle/src/test/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServletTest.java index 04d90b4413..1f93d76c92 100644 --- a/examples/bundle/src/test/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServletTest.java +++ b/examples/bundle/src/test/java/com/adobe/cq/commerce/core/examples/servlets/GraphqlServletTest.java @@ -46,6 +46,7 @@ import com.adobe.cq.commerce.core.components.internal.services.SpecificPageStrategy; import com.adobe.cq.commerce.core.components.internal.services.UrlProviderImpl; +import com.adobe.cq.commerce.core.components.internal.services.experiencefragments.CommerceExperienceFragmentsRetriever; import com.adobe.cq.commerce.core.components.internal.services.site.SiteStructureFactory; import com.adobe.cq.commerce.core.components.models.categorylist.FeaturedCategoryList; import com.adobe.cq.commerce.core.components.models.common.ProductListItem; @@ -122,6 +123,8 @@ private static AemContext createContext(String contentPath) { (Function) input -> MOCK_CONFIGURATION_OBJECT); context.registerService(Externalizer.class, Mockito.mock(Externalizer.class)); + context.registerService(CommerceExperienceFragmentsRetriever.class, + mock(CommerceExperienceFragmentsRetriever.class)); XSSAPI xssApi = mock(XSSAPI.class); when(xssApi.filterHTML(Mockito.anyString())).then(i -> i.getArgumentAt(0, String.class)); diff --git a/examples/ui.apps/src/main/content/jcr_root/apps/cif-components-examples/clientlibs/venia-theme/cif-demo.css b/examples/ui.apps/src/main/content/jcr_root/apps/cif-components-examples/clientlibs/venia-theme/cif-demo.css index df11866193..e4997838a4 100644 --- a/examples/ui.apps/src/main/content/jcr_root/apps/cif-components-examples/clientlibs/venia-theme/cif-demo.css +++ b/examples/ui.apps/src/main/content/jcr_root/apps/cif-components-examples/clientlibs/venia-theme/cif-demo.css @@ -72,4 +72,11 @@ .cif-demo .cmp-breadcrumb__item::before { content: "" !important; +} + +.cif-demo .productcollection__item-xf.test-class { + grid-row: 1; + background-color: #f5f5f5; + border: solid #e0e0e0; + padding: 1rem; } \ No newline at end of file diff --git a/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/.content.xml b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/.content.xml index 47b4478b28..1d98db32af 100644 --- a/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/.content.xml +++ b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/.content.xml @@ -26,15 +26,7 @@ sling:resourceType="core-components-examples/components/text" text="<h1>Productlist<sub>v2</sub></h1>" textIsRich="true"/> - + + + diff --git a/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/xf-productlist/page/.content.xml b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/xf-productlist/page/.content.xml new file mode 100644 index 0000000000..37aebcfd0c --- /dev/null +++ b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/commerce/productlist/xf-productlist/page/.content.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/.content.xml b/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/.content.xml new file mode 100644 index 0000000000..9dce095da4 --- /dev/null +++ b/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/.content.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/master/.content.xml b/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/master/.content.xml new file mode 100644 index 0000000000..2e15d29b9f --- /dev/null +++ b/examples/ui.content/src/main/content/jcr_root/content/experience-fragments/core-components-examples/library/commerce/commerce-experience-fragment-2/master/.content.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/productcollection/v2/productcollection/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/productcollection/v2/productcollection/_cq_dialog/.content.xml index d8be9a9863..4d0ca02624 100644 --- a/ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/productcollection/v2/productcollection/_cq_dialog/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/productcollection/v2/productcollection/_cq_dialog/.content.xml @@ -5,11 +5,14 @@ jcr:primaryType="nt:unstructured" jcr:title="Product Collection" sling:resourceType="cq/gui/components/authoring/dialog" - trackingFeature="cif-core-components:productcollection:v1"> + trackingFeature="cif-core-components:productcollection:v2"> +