ilscipio/scipio-erp

View on GitHub
applications/product/src/org/ofbiz/product/category/CategoryWorker.java

Summary

Maintainability
D
2 days
Test Coverage
/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.ofbiz.product.category;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilDateTime;
import org.ofbiz.base.util.UtilFormatOut;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilHttp;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.base.util.cache.UtilCache;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityOperator;
import org.ofbiz.entity.util.EntityQuery;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.product.catalog.CatalogWorker;
import org.ofbiz.product.product.ProductWorker;
import org.ofbiz.product.store.ProductStoreWorker;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.LocalDispatcher;
import org.ofbiz.service.ServiceUtil;

/**
 * CategoryWorker - Worker class to reduce code in JSPs.
 */
public final class CategoryWorker {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());
    public static final String SEPARATOR = "::";    // cache key separator

    public static final List<String> TOP_TRAIL = UtilMisc.unmodifiableArrayList("TOP"); // SCIPIO
    private static final UtilCache<String, List<String>> CAT_IN_CATALOG = UtilCache.createUtilCache("category.catalogs",true);

    private CategoryWorker () {}

    /**
     * Gets catalog top category.
     * <p>
     * SCIPIO: NOTE (2017-08-15): This stock method relies entirely on the session attribute and request parameter
     * <code>CATALOG_TOP_CATEGORY</code>; in stock ofbiz, it was intended to be used in backend.
     * For store implementations, the method that you most likely want
     * is {@link org.ofbiz.product.catalog.CatalogWorker#getCatalogTopCategoryId}.
     */
    public static String getCatalogTopCategory(ServletRequest request, String defaultTopCategory) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        Map<String, Object> requestParameters = UtilHttp.getParameterMap(httpRequest);
        String topCatName = null;
        boolean fromSession = false;

        // first see if a new category was specified as a parameter
        topCatName = (String) requestParameters.get("CATALOG_TOP_CATEGORY");
        // if no parameter, try from session
        if (topCatName == null) {
            topCatName = (String) httpRequest.getSession().getAttribute("CATALOG_TOP_CATEGORY");
            if (topCatName != null)
                fromSession = true;
        }
        // if nothing else, just use a default top category name
        if (topCatName == null)
            topCatName = defaultTopCategory;
        if (topCatName == null)
            topCatName = "CATALOG1";

        if (!fromSession) {
            if (Debug.infoOn()) Debug.logInfo("[CategoryWorker.getCatalogTopCategory] Setting new top category: " + topCatName, module);
            httpRequest.getSession().setAttribute("CATALOG_TOP_CATEGORY", topCatName);
        }
        return topCatName;
    }

    public static void getCategoriesWithNoParent(ServletRequest request, String attributeName) {
        Delegator delegator = (Delegator) request.getAttribute("delegator");
        Collection<GenericValue> results = new LinkedList<GenericValue>();

        try {
            Collection<GenericValue> allCategories = EntityQuery.use(delegator).from("ProductCategory").queryList();

            for (GenericValue curCat: allCategories) {
                Collection<GenericValue> parentCats = curCat.getRelated("CurrentProductCategoryRollup", null, null, true);

                if (parentCats.isEmpty()) results.add(curCat);
            }
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        request.setAttribute(attributeName, results);
    }

    public static void getRelatedCategories(ServletRequest request, String attributeName, boolean limitView) {
        Map<String, Object> requestParameters = UtilHttp.getParameterMap((HttpServletRequest) request);
        String requestId = null;

        requestId = UtilFormatOut.checkNull((String)requestParameters.get("catalog_id"), (String)requestParameters.get("CATALOG_ID"),
                (String)requestParameters.get("category_id"), (String)requestParameters.get("CATEGORY_ID"));

        if (requestId.equals(""))
            return;
        if (Debug.infoOn()) Debug.logInfo("[CategoryWorker.getRelatedCategories] RequestID: " + requestId, module);
        getRelatedCategories(request, attributeName, requestId, limitView);
    }

    public static void getRelatedCategories(ServletRequest request, String attributeName, String parentId, boolean limitView) {
        getRelatedCategories(request, attributeName, parentId, limitView, false);
    }

    public static void getRelatedCategories(ServletRequest request, String attributeName, String parentId, boolean limitView, boolean excludeEmpty) {
        List<GenericValue> categories = getRelatedCategoriesRet(request, attributeName, parentId, limitView, excludeEmpty);

        if (!categories.isEmpty())  request.setAttribute(attributeName, categories);
    }

    public static List<GenericValue> getRelatedCategoriesRet(ServletRequest request, String attributeName, String parentId, boolean limitView) {
        return getRelatedCategoriesRet(request, attributeName, parentId, limitView, false);
    }

    public static List<GenericValue> getRelatedCategoriesRet(ServletRequest request, String attributeName, String parentId, boolean limitView, boolean excludeEmpty) {
        return getRelatedCategoriesRet(request, attributeName, parentId, limitView, excludeEmpty, false);
    }

    public static List<GenericValue> getRelatedCategoriesRet(ServletRequest request, String attributeName, String parentId, boolean limitView, boolean excludeEmpty, boolean recursive) {
        Delegator delegator = (Delegator) request.getAttribute("delegator");

        return getRelatedCategoriesRet(delegator, attributeName, parentId, limitView, excludeEmpty, recursive);
    }

    public static List<GenericValue> getRelatedCategoriesRet(Delegator delegator, String attributeName, String parentId, boolean limitView, boolean excludeEmpty, boolean recursive) {
        List<GenericValue> categories = new LinkedList<GenericValue>();

        if (Debug.verboseOn()) Debug.logVerbose("[CategoryWorker.getRelatedCategories] ParentID: " + parentId, module);

        List<GenericValue> rollups = null;

        try {
            rollups = EntityQuery.use(delegator).from("ProductCategoryRollup").where("parentProductCategoryId", parentId).orderBy("sequenceNum").cache(true).queryList();
            if (limitView) {
                rollups = EntityUtil.filterByDate(rollups, true);
            }
        } catch (GenericEntityException e) {
            Debug.logWarning(e.getMessage(), module);
        }
        if (rollups != null) {
            for (GenericValue parent: rollups) {
                GenericValue cv = null;

                try {
                    cv = parent.getRelatedOne("CurrentProductCategory", true);
                } catch (GenericEntityException e) {
                    Debug.logWarning(e.getMessage(), module);
                }
                if (cv != null) {
                    if (excludeEmpty) {
                        if (!isCategoryEmpty(cv)) {
                            categories.add(cv);
                            if (recursive) {
                                categories.addAll(getRelatedCategoriesRet(delegator, attributeName, cv.getString("productCategoryId"), limitView, excludeEmpty, recursive));
                            }
                        }
                    } else {
                        categories.add(cv);
                        if (recursive) {
                            categories.addAll(getRelatedCategoriesRet(delegator, attributeName, cv.getString("productCategoryId"), limitView, excludeEmpty, recursive));
                        }
                    }
                }
            }
        }
        return categories;
    }

    public static boolean isCategoryEmpty(GenericValue category) {
        boolean empty = true;
        long members = categoryMemberCount(category);
        if (members > 0) {
            empty = false;
        }

        if (empty) {
            long rollups = categoryRollupCount(category);
            if (rollups > 0) {
                empty = false;
            }
        }

        return empty;
    }

    public static long categoryMemberCount(GenericValue category) {
        if (category == null) return 0;
        Delegator delegator = category.getDelegator();
        long count = 0;
        try {
            count = EntityQuery.use(delegator).from("ProductCategoryMember").where("productCategoryId", category.getString("productCategoryId")).queryCount();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        return count;
    }

    public static long categoryRollupCount(GenericValue category) {
        if (category == null) return 0;
        Delegator delegator = category.getDelegator();
        long count = 0;
        try {
            count = EntityQuery.use(delegator).from("ProductCategoryRollup").where("parentProductCategoryId", category.getString("productCategoryId")).queryCount();
        } catch (GenericEntityException e) {
            Debug.logError(e, module);
        }
        return count;
    }

    @SuppressWarnings("unused")
    private static EntityCondition buildCountCondition(String fieldName, String fieldValue) {
        List<EntityCondition> orCondList = new LinkedList<EntityCondition>();
        orCondList.add(EntityCondition.makeCondition("thruDate", EntityOperator.GREATER_THAN, UtilDateTime.nowTimestamp()));
        orCondList.add(EntityCondition.makeCondition("thruDate", EntityOperator.EQUALS, null));
        EntityCondition orCond = EntityCondition.makeCondition(orCondList, EntityOperator.OR);

        List<EntityCondition> andCondList = new LinkedList<EntityCondition>();
        andCondList.add(EntityCondition.makeCondition("fromDate", EntityOperator.LESS_THAN, UtilDateTime.nowTimestamp()));
        andCondList.add(EntityCondition.makeCondition(fieldName, EntityOperator.EQUALS, fieldValue));
        andCondList.add(orCond);
        EntityCondition andCond = EntityCondition.makeCondition(andCondList, EntityOperator.AND);

        return andCond;
    }

    public static void setTrail(ServletRequest request, String currentCategory) {
        Map<String, Object> requestParameters = UtilHttp.getParameterMap((HttpServletRequest) request);
        String previousCategory = (String) requestParameters.get("pcategory");
        setTrail(request, currentCategory, previousCategory);
    }

    public static void setTrail(ServletRequest request, String currentCategory, String previousCategory) {
        if (Debug.verboseOn()) Debug.logVerbose("[CategoryWorker.setTrail] Start: previousCategory=" + previousCategory + " currentCategory=" + currentCategory, module);

        // if there is no current category, just return and do nothing to that the last settings will stay
        if (UtilValidate.isEmpty(currentCategory)) {
            return;
        }

        // always get the last crumb list
        List<String> crumb = getTrail(request);
        crumb = adjustTrail(crumb, currentCategory, previousCategory);
        setTrail(request, crumb);
    }

    /**
     * Adjust the category trail.
     * <p>
     * SCIPIO: NOTE: This method performs a copy of the trail, it does not try to modify it in-place.
     */
    public static List<String> adjustTrail(List<String> origTrail, String currentCategoryId, String previousCategoryId) {
        List<String> trail = new ArrayList<>();
        if (origTrail != null) {
            trail.addAll(origTrail);
        }

        // if no previous category was specified, check to see if currentCategory is in the list
        if (UtilValidate.isEmpty(previousCategoryId)) {
            if (trail.contains(currentCategoryId)) {
                // if cur category is in crumb, remove everything after it and return
                int cindex = trail.lastIndexOf(currentCategoryId);

                if (cindex < (trail.size() - 1)) {
                    for (int i = trail.size() - 1; i > cindex; i--) {
                        trail.remove(i);
                    }
                }
                return trail;
            } else {
                // current category is not in the list, and no previous category was specified, go back to the beginning
                trail.clear();
                trail.add("TOP");
                if (UtilValidate.isNotEmpty(previousCategoryId)) {
                    trail.add(previousCategoryId);
                }
            }
        }

        if (!trail.contains(previousCategoryId)) {
            // previous category was NOT in the list, ERROR, start over
            trail.clear();
            trail.add("TOP");
            if (UtilValidate.isNotEmpty(previousCategoryId)) {
                trail.add(previousCategoryId);
            }
        } else {
            // remove all categories after the previous category, preparing for adding the current category
            int index = trail.indexOf(previousCategoryId);
            if (index < (trail.size() - 1)) {
                for (int i = trail.size() - 1; i > index; i--) {
                    trail.remove(i);
                }
            }
        }

        // add the current category to the end of the list
        trail.add(currentCategoryId);
        if (Debug.verboseOn()) Debug.logVerbose("[CategoryWorker.setTrail] Continuing list: Added currentCategory: " + currentCategoryId, module);

        return trail;
    }

    /**
     * Gets the breadcrumb trail.
     * <p>
     * SCIPIO: NOTE: 2018-11-28: The returned list is now unmodifiable (make copy, as {@link #adjustTrail(List, String, String)} already does).
     * <p>
     * SCIPIO: NOTE: 2018-11-28: This no longer uses LinkedList.
     */
    public static List<String> getTrail(ServletRequest request) {
        HttpSession session = ((HttpServletRequest) request).getSession();
        // SCIPIO: 2016-13-22: Trail must also be checked in request attributes to ensure the request is consistent
        //List<String> crumb = UtilGenerics.checkList(session.getAttribute("_BREAD_CRUMB_TRAIL_"));
        List<String> crumb = UtilGenerics.checkList(request.getAttribute("_BREAD_CRUMB_TRAIL_"));
        if (crumb == null) {
            crumb = UtilGenerics.checkList(session.getAttribute("_BREAD_CRUMB_TRAIL_"));
        }
        return crumb;
    }
    
    /**
     * SCIPIO: Version that returns a modifiable copy of the trail (or null no trail).
     */
    public static List<String> getTrailCopy(ServletRequest request) {
        List<String> crumb = getTrail(request);
        return (crumb != null) ? new ArrayList<>(crumb) : null;
    }

    /**
     * SCIPIO: Version that returns a copy of the trail without the TOP category.
     */
    public static List<String> getTrailNoTop(ServletRequest request) {
        List<String> fullTrail = getTrail(request);
        List<String> res = null;
        if (fullTrail != null) {
            res = new ArrayList<>(fullTrail.size());
            Iterator<String> it = fullTrail.iterator();
            while (it.hasNext()) {
                String next = it.next(); // check first
                if (!"TOP".equals(next)) {
                    res.add(next);
                }
            }
        }
        return res;
    }

    /**
     * SCIPIO: Version that returns a copy of the trail without the ones listed in the exceptions parameter.
     */
    public static List<String> getCustomTrail(ServletRequest request, List<String> exceptions) {
        List<String> fullTrail = getTrail(request);
        List<String> res = null;
        if (fullTrail != null) {
            res = new ArrayList<>(fullTrail.size());
            Iterator<String> it = fullTrail.iterator();
            while (it.hasNext()) {
                String next = it.next(); // check first
                if (!exceptions.contains(next)) {
                    res.add(next);
                }
            }
        }
        return res;
    }

    /**
     * Sets breadcrumbs trail to the exact given value.
     * <p>
     * SCIPIO: This is modified to accept a onlyIfNewInRequest boolean that will check to see
     * if a breadcrumb was already set in the request. Default is false.
     * This is needed in some places to prevent squashing breadcrumbs set in servlets and filters.
     */
    public static List<String> setTrail(ServletRequest request, List<String> crumb, boolean onlyIfNewInRequest) {
        HttpSession session = ((HttpServletRequest) request).getSession();
        // SCIPIO: 2018-11-28: Trail must never be modified in-place (thread safety)
        if (crumb instanceof ArrayList || crumb instanceof LinkedList) {
            crumb = Collections.unmodifiableList(crumb);
        }
        
        if (onlyIfNewInRequest) {
            // SCIPIO: Check if was already set
            if (request.getAttribute("_BREAD_CRUMB_TRAIL_") == null) {
                session.setAttribute("_BREAD_CRUMB_TRAIL_", crumb);
                // SCIPIO: 2016-13-22: Trail must also be set in request attributes to ensure the request is consistent
                request.setAttribute("_BREAD_CRUMB_TRAIL_", crumb);
            }
        } else {
            // SCIPIO: stock case
            session.setAttribute("_BREAD_CRUMB_TRAIL_", crumb);
            // SCIPIO: 2016-13-22: Trail must also be set in request attributes to ensure the request is consistent
            request.setAttribute("_BREAD_CRUMB_TRAIL_", crumb);
        }
        return crumb;
    }

    /**
     * Sets breadcrumbs trail to the exact given value.
     */
    public static List<String> setTrail(ServletRequest request, List<String> crumb) {
        return setTrail(request, crumb, false);
    }

    /**
     * SCIPIO: Sets breadcrumbs trail to the exact given value but only if not yet set during request.
     */
    public static List<String> setTrailIfFirstInRequest(ServletRequest request, List<String> crumb) {
        return setTrail(request, crumb, true);
    }

    /**
     * SCIPIO: Resets the breadcrumb to the "TOP".
     * Added 2019-01.
     */
    public static List<String> resetTrail(ServletRequest request) {
        return setTrail(request, TOP_TRAIL, false);
    }

    public static boolean checkTrailItem(ServletRequest request, String category) {
        List<String> crumb = getTrail(request);

        if (crumb != null && crumb.contains(category)) {
            return true;
        } else {
            return false;
        }
    }

    public static String lastTrailItem(ServletRequest request) {
        List<String> crumb = getTrail(request);

        if (UtilValidate.isNotEmpty(crumb)) {
            return crumb.get(crumb.size() - 1);
        } else {
            return null;
        }
    }

    // SCIPIO: Added moment and useCache support, overloads
    public static boolean isProductInCategory(Delegator delegator, String productId, String productCategoryId, Timestamp moment, boolean useCache) throws GenericEntityException {
        if (productCategoryId == null) return false;
        if (UtilValidate.isEmpty(productId)) return false;

        List<GenericValue> productCategoryMembers = EntityQuery.use(delegator).from("ProductCategoryMember")
                .where("productCategoryId", productCategoryId, "productId", productId)
                .cache(useCache)
                .filterByDate(moment)
                .queryList();
        if (UtilValidate.isEmpty(productCategoryMembers)) {
            //before giving up see if this is a variant product, and if so look up the virtual product and check it...
            GenericValue product = EntityQuery.use(delegator).from("Product").where("productId", productId).cache(useCache).queryOne();
            List<GenericValue> productAssocs = ProductWorker.getVariantVirtualAssocs(product, moment, useCache);
            //this does take into account that a product could be a variant of multiple products, but this shouldn't ever really happen...
            if (productAssocs != null) {
                for (GenericValue productAssoc: productAssocs) {
                    if (isProductInCategory(delegator, productAssoc.getString("productId"), productCategoryId, moment, useCache)) {
                        return true;
                    }
                }
            }
            return false;
        } else {
            return true;
        }
    }

    public static boolean isProductInCategory(Delegator delegator, String productId, String productCategoryId, boolean useCache) throws GenericEntityException {
        return isProductInCategory(delegator, productId, productCategoryId, UtilDateTime.nowTimestamp(), useCache);
    }

    public static boolean isProductInCategory(Delegator delegator, String productId, String productCategoryId) throws GenericEntityException {
        return isProductInCategory(delegator, productId, productCategoryId, UtilDateTime.nowTimestamp(), true);
    }

    /**
     * Checks if the given product is member of any of the passed category IDs (SCIPIO).
     * NOTE: This is optimized for a large number of productCategoryIds being passed.
     * @see ProductWorker#getCategoryIdsForProduct
     */
    public static boolean isProductInCategories(Delegator delegator, String productId, Collection<String> productCategoryIds, Timestamp moment, boolean useCache) throws GenericEntityException {
        if (UtilValidate.isEmpty(productId)) {
            return false;
        }
        return isProductInCategories(delegator, productId, null, productCategoryIds, moment, useCache);
    }

    /**
     * Checks if the given product is member of any of the passed category IDs (SCIPIO).
     * NOTE: This is optimized for a large number of productCategoryIds being passed.
     * @see ProductWorker#getCategoryIdsForProduct
     */
    public static boolean isProductInCategories(Delegator delegator, String productId, Collection<String> productCategoryIds, boolean useCache) throws GenericEntityException {
        if (UtilValidate.isEmpty(productId)) {
            return false;
        }
        return isProductInCategories(delegator, productId, null, productCategoryIds, UtilDateTime.nowTimestamp(), useCache);
    }

    /**
     * Checks if the given product is member of any of the passed category IDs (SCIPIO).
     * NOTE: This is optimized for a large number of productCategoryIds being passed.
     * @see ProductWorker#getCategoryIdsForProduct
     */
    public static boolean isProductInCategories(Delegator delegator, GenericValue product, Collection<String> productCategoryIds, Timestamp moment, boolean useCache) throws GenericEntityException {
        if (product == null) {
            return false;
        }
        return isProductInCategories(delegator, product.getString("productId"), product, productCategoryIds, moment, useCache);
    }

    /**
     * Checks if the given product is member of any of the passed category IDs (SCIPIO).
     * NOTE: This is optimized for a large number of productCategoryIds being passed.
     * @see ProductWorker#getCategoryIdsForProduct
     */
    public static boolean isProductInCategories(Delegator delegator, GenericValue product, Collection<String> productCategoryIds, boolean useCache) throws GenericEntityException {
        if (product == null) {
            return false;
        }
        return isProductInCategories(delegator, product.getString("productId"), product, productCategoryIds, UtilDateTime.nowTimestamp(), useCache);
    }

    private static boolean isProductInCategories(Delegator delegator, String productId, GenericValue product, Collection<String> productCategoryIds, Timestamp moment, boolean useCache) throws GenericEntityException {
        if (UtilValidate.isEmpty(productCategoryIds)) {
            return false;
        }
        List<GenericValue> productCategoryMembers = EntityQuery.use(delegator).from("ProductCategoryMember")
                .select("productCategoryId")
                .where("productId", productId)
                .cache(useCache)
                .filterByDate(moment)
                .queryList();
        for(GenericValue pcm : productCategoryMembers) {
            if (productCategoryIds.contains(pcm.getString("productCategoryId"))) {
                return true;
            }
        }
        // before giving up see if this is a variant product, and if so look up the virtual product and check it...
        if (product == null) {
            product = delegator.from("Product").where("productId", productId).cache(useCache).queryOne();
        }
        List<GenericValue> productAssocs = ProductWorker.getVariantVirtualAssocs(product, moment, useCache);
        // this does take into account that a product could be a variant of multiple products, but this shouldn't ever really happen...
        if (productAssocs != null) {
            for (GenericValue productAssoc: productAssocs) {
                if (isProductInCategories(delegator, productAssoc.getString("productId"), null, productCategoryIds, moment, useCache)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * SCIPIO: Returns true only if the category contains the product, NON-recursive.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryContainsProduct(Delegator delegator, LocalDispatcher dispatcher, String productCategoryId, String productId) {
        if (UtilValidate.isEmpty(productCategoryId) || UtilValidate.isEmpty(productId)) {
            return false;
        }
        try {
            List<EntityCondition> conds = new ArrayList<>();
            conds.add(EntityCondition.makeCondition("productCategoryId", productCategoryId));
            conds.add(EntityCondition.makeCondition("productId", productId));
            List<GenericValue> productCategoryMembers = EntityQuery.use(delegator).select("productCategoryId").from("ProductCategoryMember")
                    .where(conds).filterByDate().cache(true).queryList();
            return !productCategoryMembers.isEmpty();
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        return false; // can't tell, return false to play it safe
    }

    /**
     * SCIPIO: Returns true only if the category contains the product, NON-recursive.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryContainsProduct(ServletRequest request, String productCategoryId, String productId) {
        return isCategoryContainsProduct((Delegator) request.getAttribute("delegator"),
                (LocalDispatcher) request.getAttribute("dispatcher"), productCategoryId, productId);
    }

    public static List<GenericValue> filterProductsInCategory(Delegator delegator, List<GenericValue> valueObjects, String productCategoryId) throws GenericEntityException {
        return filterProductsInCategory(delegator, valueObjects, productCategoryId, "productId");
    }

    public static List<GenericValue> filterProductsInCategory(Delegator delegator, List<GenericValue> valueObjects, String productCategoryId, String productIdFieldName) throws GenericEntityException {
        List<GenericValue> newList = new LinkedList<GenericValue>();

        if (productCategoryId == null) return newList;
        if (valueObjects == null) return null;

        for (GenericValue curValue: valueObjects) {
            String productId = curValue.getString(productIdFieldName);
            if (isProductInCategory(delegator, productId, productCategoryId)) {
                newList.add(curValue);
            }
        }
        return newList;
    }

    public static void getCategoryContentWrappers(Map<String, CategoryContentWrapper> catContentWrappers, List<GenericValue> categoryList, HttpServletRequest request) throws GenericEntityException {
        if (catContentWrappers == null || categoryList == null) {
            return;
        }
        for (GenericValue cat: categoryList) {
            String productCategoryId = (String) cat.get("productCategoryId");

            if (catContentWrappers.containsKey(productCategoryId)) {
                // if this ID is already in the Map, skip it (avoids inefficiency, infinite recursion, etc.)
                continue;
            }

            CategoryContentWrapper catContentWrapper = new CategoryContentWrapper(cat, request);
            catContentWrappers.put(productCategoryId, catContentWrapper);
            List<GenericValue> subCat = getRelatedCategoriesRet(request, "subCatList", productCategoryId, true);
            getCategoryContentWrappers(catContentWrappers, subCat, request);
        }
    }

    /**
     * Returns a complete category trail - can be used for exporting proper category trees. 
     * This is mostly useful when used in combination with bread-crumbs,  for building a 
     * faceted index tree, or to export a category tree for migration to another system.
     * Will create the tree from root point to categoryId.
     *
     * This method is not meant to be run on every request.
     * Its best use is to generate the trail every so often and store somewhere 
     * (a lucene/solr tree, entities, cache or so). 
     *
     * @param dctx The DispatchContext that this service is operating in
     * @param context Map containing the input parameters
     * @return Map organized trail from root point to categoryId.
     */
    public static Map<String, Object> getCategoryTrail(DispatchContext dctx, Map<String, ?> context) {
        String productCategoryId = (String) context.get("productCategoryId");
        Map<String, Object> results = ServiceUtil.returnSuccess();
        Delegator delegator = dctx.getDelegator();
        List<String> trailElements = new LinkedList<String>();
        trailElements.add(productCategoryId);
        String parentProductCategoryId = productCategoryId;
        while (UtilValidate.isNotEmpty(parentProductCategoryId)) {
            // find product category rollup
            try {
                List<EntityCondition> rolllupConds = new LinkedList<EntityCondition>();
                rolllupConds.add(EntityCondition.makeCondition("productCategoryId", parentProductCategoryId));
                List<GenericValue> productCategoryRollups = EntityQuery.use(delegator).from("ProductCategoryRollup").where(rolllupConds).orderBy("sequenceNum")
                        .filterByDate().cache(true).queryList(); // SCIPIO: moved filterByDate into EntityQuery 
                if (UtilValidate.isNotEmpty(productCategoryRollups)) {
                    // add only categories that belong to the top category to trail
                    for (GenericValue productCategoryRollup : productCategoryRollups) {
                        String trailCategoryId = productCategoryRollup.getString("parentProductCategoryId");
                        parentProductCategoryId = trailCategoryId;
                        if (trailElements.contains(trailCategoryId)) {
                            break;
                        } else {
                            trailElements.add(trailCategoryId);
                        }
                    }
                } else {
                    parentProductCategoryId = null;
                }
            } catch (GenericEntityException e) {
                Map<String, String> messageMap = UtilMisc.toMap("errMessage", ". Cannot generate trail from product category. ");
                String errMsg = UtilProperties.getMessage("CommonUiLabels", "CommonDatabaseProblem", messageMap, (Locale) context.get("locale"));
                Debug.logError(e, errMsg, module);
                return ServiceUtil.returnError(errMsg);
            }
        }
        Collections.reverse(trailElements);
        results.put("trail", trailElements);
        return results;
    }


    /**
     *  SCIPIO: Returns a List of productStore specific catalogIds a category is assigned to for a given productstoreId
     * */
    public static List<String> getProductstoreCategoryCatalogIds(Delegator delegator, String productStoreId, String productCategoryId) {
        List<String> productStoreCategoryCatalogIds = new ArrayList<String>();
        try {
            String cacheKey = productStoreId+SEPARATOR+productCategoryId;

            List<String> cachedValue = CAT_IN_CATALOG.get(cacheKey);
            if (cachedValue != null) {
                return cachedValue;
            }

            List<GenericValue> catalogs = CatalogWorker.getStoreCatalogs(delegator, productStoreId);
            List<String> storeCatalogIds = catalogs.stream().map(x -> x.getString("prodCatalogId"))
                    .collect(Collectors.toList());
            List<List<String>> catRollups = CategoryWorker.getCategoryRollupTrails(delegator,productCategoryId,true);
            List<String> categoryCatalogIds = catRollups.stream().map(x -> x.get(0))
                    .collect(Collectors.toList());
            List<String> allProdCatalogCategories = new ArrayList<String>();


            for(String catalogId : storeCatalogIds){
                List<GenericValue> pccList = CatalogWorker.getProdCatalogCategories(delegator, catalogId, null, UtilDateTime.nowTimestamp(), false, true);
                for(GenericValue pcc : pccList){
                    allProdCatalogCategories.add(pcc.getString("productCategoryId"));
                }
            }

            for(String categoryCatalogId : categoryCatalogIds){
                if(allProdCatalogCategories.contains(categoryCatalogId)){
                    productStoreCategoryCatalogIds.add(categoryCatalogId);
                }
            }

            CAT_IN_CATALOG.put(cacheKey,productStoreCategoryCatalogIds);
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        return productStoreCategoryCatalogIds;
    }


    /**
     * SCIPIO: Returns true only if the category ID is connected to the productStore
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryUsedByProductStore(Delegator delegator, String productStoreId, String productCategoryId){
        return UtilValidate.isNotEmpty(getProductstoreCategoryCatalogIds(delegator,productStoreId,productCategoryId));
    }

    public static boolean isCategoryUsedByProductStore(ServletRequest request,String productCategoryId) {
        String productStoreId = ProductStoreWorker.getProductStoreId(request);
        return isCategoryUsedByProductStore((Delegator) request.getAttribute("delegator"),productStoreId,productCategoryId);
    }


    /**
     * SCIPIO: Returns true only if the category ID is child of the given parent category ID.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryChildOf(Delegator delegator, LocalDispatcher dispatcher, String parentProductCategoryId, String productCategoryId) {
        try {
            List<EntityCondition> rolllupConds = new ArrayList<>();
            rolllupConds.add(EntityCondition.makeCondition("parentProductCategoryId", parentProductCategoryId));
            rolllupConds.add(EntityCondition.makeCondition("productCategoryId", productCategoryId));
            Collection<GenericValue> rollups = EntityQuery.use(delegator).from("ProductCategoryRollup").where(rolllupConds).filterByDate().cache().queryList();
            return !rollups.isEmpty();
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        return false;
    }

    /**
     * SCIPIO: Returns true only if the category ID is child of the given parent category ID.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryChildOf(ServletRequest request, String parentProductCategoryId, String productCategoryId) {
        return isCategoryChildOf((Delegator) request.getAttribute("delegator"), (LocalDispatcher) request.getAttribute("dispatcher"),
                parentProductCategoryId, productCategoryId);
    }

    /**
     * SCIPIO: Returns true only if the category ID is a top category.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryTop(Delegator delegator, LocalDispatcher dispatcher, String productCategoryId) {
        if (UtilValidate.isEmpty(productCategoryId)) {
            return false;
        }
        try {
            List<EntityCondition> rollupConds = new ArrayList<>();
            rollupConds.add(EntityCondition.makeCondition("productCategoryId", productCategoryId));
            Collection<GenericValue> rollups = EntityQuery.use(delegator).from("ProductCategoryRollup").where(rollupConds).filterByDate().cache().queryList();
            return rollups.isEmpty();
        } catch (GenericEntityException e) {
            Debug.logWarning(e, module);
        }
        return false; // can't tell, return false to play it safe
    }

    /**
     * SCIPIO: Returns true only if the category ID is a top category.
     * <p>
     * NOTE: is caching
     */
    public static boolean isCategoryTop(ServletRequest request, String productCategoryId) {
        return isCategoryTop((Delegator) request.getAttribute("delegator"),
                (LocalDispatcher) request.getAttribute("dispatcher"), productCategoryId);
    }

    /**
     * SCIPIO: Returns a valid category path/trail (as parts) from the given trail,
     * starting with the top category (but without the fake "TOP" category).
     * If none could be determined, returns null.
     * <p>
     * In some circumstances, getTrailNoTop alone could work, but we need a method that guarantees
     * a full path because the trail may not always be reliable or formal enough.
     * <p>
     * FIXME: This does NOT currently verify that the category path is actually valid!
     * We are relying on trailbuilding code elsewhere until can determine a reliable algorithm.
     * <p>
     * TODO?: This could try to produce better guesses in cases where top category is missing, instead
     * of returning null? I don't know if this is always safe, would want an extra flag and maybe
     * should not be the default behavior.
     */
    public static List<String> getCategoryPathFromTrailAsList(ServletRequest request, List<String> trail) {
        List<String> path = null;
        //List<String> trail = getTrailNoTop(request);
        // Get the last trail entry that is a top category. Usually this will be the first after "TOP",
        // but the trail is sometimes strange, so need to guarantee at least that.
        if (trail != null) {
            ListIterator<String> it = trail.listIterator(trail.size());
            while (it.hasPrevious() && path == null) {
                String part = it.previous();
                if (UtilValidate.isNotEmpty(part) && !"TOP".equals(part) && isCategoryTop(request, part)) {
                    path = new ArrayList<String>(trail.subList(it.nextIndex(), trail.size()));
                }
            }
        }
        /* TODO: should validate the path here
        if (path != null) {

        }*/
        return path;
    }

    /**
     * SCIPIO: Returns a valid category path/trail (as parts) from the current request trail in session,
     * starting with the top category (but without the fake "TOP" category).
     * If none could be determined, returns null.
     */
    public static List<String> getCategoryPathFromTrailAsList(ServletRequest request) {
        return getCategoryPathFromTrailAsList(request, getTrail(request));
    }

    /**
     * SCIPIO: Checks the given trail for the last recorded top category ID, if any.
     * This can be the catalog top category or a different one.
     * <p>
     * NOTE: is caching
     */
    public static String getTopCategoryFromTrail(Delegator delegator, LocalDispatcher dispatcher, List<String> trail) {
        String catId = null;
        if (trail != null) {
            ListIterator<String> it = trail.listIterator(trail.size());
            while (it.hasPrevious()) {
                catId = it.previous();
                if (UtilValidate.isNotEmpty(catId) && !"TOP".equals(catId)) {
                    if (isCategoryTop(delegator, dispatcher, catId)) {
                        return catId;
                    }
                }
            }
        }
        return null;
    }


    /**
     * SCIPIO: Checks the given trail for the last recorded top category ID, if any.
     * This can be the catalog top category or a different one.
     */
    public static String getTopCategoryFromTrail(ServletRequest request, List<String> trail) {
        return getTopCategoryFromTrail((Delegator) request.getAttribute("delegator"),
                (LocalDispatcher) request.getAttribute("dispatcher"), trail);
    }

    /**
     * SCIPIO: Checks the current trail for the last recorded top category ID, if any.
     * This can be the catalog top category or a different one.
     */
    public static String getTopCategoryFromTrail(ServletRequest request) {
        return getTopCategoryFromTrail(request, getTrail(request));
    }


    /**
     * SCIPIO: Attempts to determine a suitable category for the given product from given trail.
     */
    public static String getCategoryForProductFromTrail(ServletRequest request, String productId, List<String> trail) {
        if (UtilValidate.isNotEmpty(productId)) {
            if (trail != null && !trail.isEmpty()) {
                String catId = trail.get(trail.size() - 1);
                if (UtilValidate.isNotEmpty(catId) && !"TOP".equals(catId)) {
                    if (CategoryWorker.isCategoryContainsProduct(request, catId, productId)) {
                        return catId;
                    }
                }
            }
        }
        return null;
    }

    /**
     * SCIPIO: Attempts to determine a suitable category for the given product from the trail in session.
     */
    public static String getCategoryForProductFromTrail(ServletRequest request, String productId) {
        return getCategoryForProductFromTrail(request, productId, CategoryWorker.getTrail(request));
    }

    /**
     * SCIPIO: For each simple-text-compatible prodCatContentTypeId, returns a list of complex record views,
     * where the first entry is ProductCategoryContentAndElectronicText and the following entries (if any)
     * are ContentAssocToElectronicText views.
     * <p>
     * NOTE: If there are multiple ProductCategoryContent for same cat/type, this fetches the lastest only (logs warning).
     * System or user is expected to prevent this.
     * <p>
     * filterByDate must be set to a value in order to filter by date.
     * Added 2017-10-27.
     */
    public static Map<String, List<GenericValue>> getProductCategoryContentLocalizedSimpleTextViews(Delegator delegator, LocalDispatcher dispatcher,
            String productCategoryId, Collection<String> prodCatContentTypeIdList, java.sql.Timestamp filterByDate, boolean useCache) throws GenericEntityException {
        Map<String, List<GenericValue>> fieldMap = new HashMap<>();

        List<EntityCondition> typeIdCondList = new ArrayList<>(prodCatContentTypeIdList.size());
        if (prodCatContentTypeIdList != null) {
            for(String prodCatContentTypeId : prodCatContentTypeIdList) {
                typeIdCondList.add(EntityCondition.makeCondition("prodCatContentTypeId", prodCatContentTypeId));
            }
        }
        List<EntityCondition> condList = new ArrayList<>();
        condList.add(EntityCondition.makeCondition("productCategoryId", productCategoryId));
        if (typeIdCondList.size() > 0) {
            condList.add(EntityCondition.makeCondition(typeIdCondList, EntityOperator.OR));
        }
        condList.add(EntityCondition.makeCondition("drDataResourceTypeId", "ELECTRONIC_TEXT"));

        EntityQuery query = EntityQuery.use(delegator).from("ProductCategoryContentAndElectronicText")
                .where(condList).orderBy("-fromDate").cache(useCache);
        if (filterByDate != null) {
            query = query.filterByDate(filterByDate);
        }
        List<GenericValue> prodCatContentList = query.queryList();
        for(GenericValue prodCatContent : prodCatContentList) {
            String prodCatContentTypeId = prodCatContent.getString("prodCatContentTypeId");
            if (fieldMap.containsKey(prodCatContentTypeId)) {
                Debug.logWarning("getProductCategoryContentLocalizedSimpleTextViews: multiple ProductCategoryContentAndElectronicText"
                        + " records found for prodCatContentTypeId '" + prodCatContentTypeId + "' for productCategoryId '" + productCategoryId + "'; "
                        + " returning first found only (this may cause unexpected texts to appear)", module);
                continue;
            }
            String contentIdStart = prodCatContent.getString("contentId");

            condList = new ArrayList<>();
            condList.add(EntityCondition.makeCondition("contentIdStart", contentIdStart));
            condList.add(EntityCondition.makeCondition("contentAssocTypeId", "ALTERNATE_LOCALE"));
            condList.add(EntityCondition.makeCondition("drDataResourceTypeId", "ELECTRONIC_TEXT"));
            query = EntityQuery.use(delegator).from("ContentAssocToElectronicText")
                    .where(condList).orderBy("-fromDate").cache(useCache);
            if (filterByDate != null) {
                query = query.filterByDate(filterByDate);
            }
            List<GenericValue> contentAssocList = query.queryList();
            List<GenericValue> valueList = new ArrayList<>(contentAssocList.size() + 1);
            valueList.add(prodCatContent);
            valueList.addAll(contentAssocList);
            fieldMap.put(prodCatContentTypeId, valueList);
        }

        return fieldMap;
    }


    /**
     * SCIPIO: Returns all rollups for a category.
     * Imported from SolrCategoryUtil, 2017-11-09.
     */
    public static List<List<String>> getCategoryRollupTrails(Delegator delegator, String productCategoryId, Timestamp moment, boolean ordered, boolean useCache) {
        List<List<String>> trailElements = new ArrayList<>();
        try {
            // NOTE: Can't filter on sequenceNum because it only makes sense if querying by parentProductCategoryId
            List<GenericValue> productCategoryRollups = EntityQuery.use(delegator).from("ProductCategoryRollup")
                    .where("productCategoryId", productCategoryId).orderBy(ordered ? UtilMisc.toList("sequenceNum") : null).filterByDate(moment).cache(useCache).queryList();
            if (UtilValidate.isNotEmpty(productCategoryRollups)) {
                // For each parent cat, get its trails recursively and add our own
                for (GenericValue productCategoryRollup : productCategoryRollups) {
                    String parentProductCategoryId = productCategoryRollup.getString("parentProductCategoryId");
                    List<List<String>> parentTrails = getCategoryRollupTrails(delegator, parentProductCategoryId, moment, ordered, useCache);
                    for (List<String> trail : parentTrails) {
                        // WARN: modifying the parent trail in-place for speed
                        trail.add(productCategoryId);
                        trailElements.add(trail);
                    }
                }
            }
        } catch (GenericEntityException e) {
            Debug.logError(e, "Cannot generate trail from product category '" + productCategoryId + "'", module);
        }
        if (trailElements.isEmpty()) {
            List<String> trailElement = new ArrayList<>();
            trailElement.add(productCategoryId);
            trailElements.add(trailElement);
        }
        return trailElements;
    }

    public static List<List<String>> getCategoryRollupTrails(Delegator delegator, String productCategoryId, boolean useCache) {
        return getCategoryRollupTrails(delegator, productCategoryId, UtilDateTime.nowTimestamp(), true, useCache);
    }

    /**
     * SCIPIO: Returns all rollups for a category that have the given top categories.
     * TODO: REVIEW: maybe this can be optimized with a smarter algorithm?
     * Added 2017-11-09.
     */
    public static List<List<String>> getCategoryRollupTrails(Delegator delegator, String productCategoryId, Collection<String> topCategoryIds, Timestamp moment, boolean ordered, boolean useCache) {
        List<List<String>> trails = getCategoryRollupTrails(delegator, productCategoryId, moment, ordered, useCache);
        if (topCategoryIds == null) return trails;
        List<List<String>> filtered = new ArrayList<>(trails.size());
        for(List<String> trail : trails) {
            if (!trail.isEmpty() && topCategoryIds.contains(trail.get(0))) {
                filtered.add(trail);
            }
        }
        return filtered;
    }

    public static List<List<String>> getCategoryRollupTrails(Delegator delegator, String productCategoryId, Collection<String> topCategoryIds, boolean useCache) {
        return getCategoryRollupTrails(delegator, productCategoryId, topCategoryIds, UtilDateTime.nowTimestamp(), true, useCache);
    }


    public static <C extends Collection<String>> C getAllCatalogCategoryIds(C outCategoryIds, Delegator delegator, String catalogId, Collection<String> prodCatalogCategoryTypeIds,
                                                       Timestamp moment, boolean ordered, boolean useCache) throws GenericEntityException {
        List<GenericValue> pccList = CatalogWorker.getProdCatalogCategories(delegator, catalogId, prodCatalogCategoryTypeIds, moment, false, useCache);
        getAllCatalogCategoryIdsImpl(outCategoryIds, delegator, pccList, moment, ordered, useCache);
        return outCategoryIds;
    }

    private static <C extends Collection<String>> void getAllCatalogCategoryIdsImpl(C outCategoryIds, Delegator delegator, List<GenericValue> catList,
                                                 Timestamp moment, boolean ordered, boolean useCache) throws GenericEntityException {
        for(GenericValue cat : catList) {
            String categoryId = cat.getString("productCategoryId");
            outCategoryIds.add(categoryId);
            List<GenericValue> rollups = delegator.from("ProductCategoryRollup").where("parentProductCategoryId", categoryId).filterByDate(moment).cache(useCache).queryList();
            getAllCatalogCategoryIdsImpl(outCategoryIds, delegator, rollups, moment, ordered, useCache);
        }
    }

}