/******************************************************************************* * 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.apache.ofbiz.product.feature; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilGenerics; import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtil; import org.apache.ofbiz.service.DispatchContext; import org.apache.ofbiz.service.GenericServiceException; import org.apache.ofbiz.service.LocalDispatcher; import org.apache.ofbiz.service.ModelService; import org.apache.ofbiz.service.ServiceUtil; /** * Services for product features */ public class ProductFeatureServices { public static final String module = ProductFeatureServices.class.getName(); public static final String resource = "ProductUiLabels"; /* * Parameters: productFeatureCategoryId, productFeatureGroupId, productId, productFeatureApplTypeId * Result: productFeaturesByType, a Map of all product features from productFeatureCategoryId, group by productFeatureType -> List of productFeatures * If the parameter were productFeatureCategoryId, the results are from ProductFeatures. If productFeatureCategoryId were null and there were a productFeatureGroupId, * the results are from ProductFeatureGroupAndAppl. Otherwise, if there is a productId, the results are from ProductFeatureAndAppl. * The optional productFeatureApplTypeId causes results to be filtered by this parameter--only used in conjunction with productId. */ public static Map<String, Object> getProductFeaturesByType(DispatchContext dctx, Map<String, ? extends Object> context) { Map<String, Object> results = new HashMap<String, Object>(); Delegator delegator = dctx.getDelegator(); Locale locale = (Locale) context.get("locale"); /* because we might need to search either for product features or for product features of a product, the search code has to be generic. * we will determine which entity and field to search on based on what the user has supplied us with. */ String valueToSearch = (String) context.get("productFeatureCategoryId"); String productFeatureApplTypeId = (String) context.get("productFeatureApplTypeId"); String entityToSearch = "ProductFeature"; String fieldToSearch = "productFeatureCategoryId"; List<String> orderBy = UtilMisc.toList("productFeatureTypeId", "description"); if (valueToSearch == null && context.get("productFeatureGroupId") != null) { entityToSearch = "ProductFeatureGroupAndAppl"; fieldToSearch = "productFeatureGroupId"; valueToSearch = (String) context.get("productFeatureGroupId"); // use same orderBy as with a productFeatureCategoryId search } else if (valueToSearch == null && context.get("productId") != null) { entityToSearch = "ProductFeatureAndAppl"; fieldToSearch = "productId"; valueToSearch = (String) context.get("productId"); orderBy = UtilMisc.toList("sequenceNum", "productFeatureApplTypeId", "productFeatureTypeId", "description"); } if (valueToSearch == null) { return ServiceUtil.returnError(UtilProperties.getMessage(resource, "ProductFeatureByType", locale)); } try { // get all product features in this feature category List<GenericValue> allFeatures = EntityQuery.use(delegator).from(entityToSearch).where(fieldToSearch, valueToSearch).orderBy(orderBy).queryList(); if (entityToSearch.equals("ProductFeatureAndAppl") && productFeatureApplTypeId != null) allFeatures = EntityUtil.filterByAnd(allFeatures, UtilMisc.toMap("productFeatureApplTypeId", productFeatureApplTypeId)); List<String> featureTypes = new LinkedList<String>(); Map<String, List<GenericValue>> featuresByType = new LinkedHashMap<String, List<GenericValue>>(); for (GenericValue feature: allFeatures) { String featureType = feature.getString("productFeatureTypeId"); if (!featureTypes.contains(featureType)) { featureTypes.add(featureType); } List<GenericValue> features = featuresByType.get(featureType); if (features == null) { features = new LinkedList<GenericValue>(); featuresByType.put(featureType, features); } features.add(feature); } results = ServiceUtil.returnSuccess(); results.put("productFeatureTypes", featureTypes); results.put("productFeaturesByType", featuresByType); } catch (GenericEntityException ex) { Debug.logError(ex, ex.getMessage(), module); return ServiceUtil.returnError(ex.getMessage()); } return results; } /* * Parameter: productId, productFeatureAppls (a List of ProductFeatureAndAppl entities of features applied to productId) * Result: variantProductIds: a List of productIds of variants with those features */ public static Map<String, Object> getAllExistingVariants(DispatchContext dctx, Map<String, ? extends Object> context) { Map<String, Object> results = new HashMap<String, Object>(); Delegator delegator = dctx.getDelegator(); String productId = (String) context.get("productId"); List<String> curProductFeatureAndAppls = UtilGenerics.checkList(context.get("productFeatureAppls")); List<String> existingVariantProductIds = new LinkedList<String>(); try { /* * get a list of all products which are associated with the current one as PRODUCT_VARIANT and for each one, * see if it has every single feature in the list of productFeatureAppls as a STANDARD_FEATURE. If so, then * it qualifies and add it to the list of existingVariantProductIds. */ List<GenericValue> productAssocs = EntityQuery.use(delegator).from("ProductAssoc").where("productId", productId, "productAssocTypeId", "PRODUCT_VARIANT").filterByDate().queryList(); for (GenericValue productAssoc: productAssocs) { //for each associated product, if it has all standard features, display it's productId boolean hasAllFeatures = true; for (String productFeatureAndAppl: curProductFeatureAndAppls) { Map<String, String> findByMap = UtilMisc.toMap("productId", productAssoc.getString("productIdTo"), "productFeatureId", productFeatureAndAppl, "productFeatureApplTypeId", "STANDARD_FEATURE"); List<GenericValue> standardProductFeatureAndAppls = EntityQuery.use(delegator).from("ProductFeatureAppl").where(findByMap).filterByDate().queryList(); if (UtilValidate.isEmpty(standardProductFeatureAndAppls)) { hasAllFeatures = false; break; } else { } } if (hasAllFeatures) { // add to list of existing variants: productId=productAssoc.productIdTo existingVariantProductIds.add(productAssoc.getString("productIdTo")); } } results = ServiceUtil.returnSuccess(); results.put("variantProductIds", existingVariantProductIds); } catch (GenericEntityException ex) { Debug.logError(ex, ex.getMessage(), module); return ServiceUtil.returnError(ex.getMessage()); } return results; } /* * Parameter: productId (of the parent product which has SELECTABLE features) * Result: featureCombinations, a List of Maps containing, for each possible variant of the productid: * {defaultVariantProductId: id of this variant; curProductFeatureAndAppls: features applied to this variant; existingVariantProductIds: List of productIds which are already variants with these features } */ public static Map<String, Object> getVariantCombinations(DispatchContext dctx, Map<String, ? extends Object> context) { Map<String, Object> results = new HashMap<String, Object>(); LocalDispatcher dispatcher = dctx.getDispatcher(); String productId = (String) context.get("productId"); try { Map<String, Object> featuresResults = dispatcher.runSync("getProductFeaturesByType", UtilMisc.toMap("productId", productId)); Map<String, List<GenericValue>> features; if (featuresResults.get(ModelService.RESPONSE_MESSAGE).equals(ModelService.RESPOND_SUCCESS)) { features = UtilGenerics.checkMap(featuresResults.get("productFeaturesByType")); } else { return ServiceUtil.returnError((String) featuresResults.get(ModelService.ERROR_MESSAGE_LIST)); } // need to keep 2 lists, oldCombinations and newCombinations, and keep swapping them after each looping. Otherwise, you'll get a // concurrent modification exception List<Map<String, Object>> oldCombinations = new LinkedList<Map<String,Object>>(); // loop through each feature type for (Map.Entry<String, List<GenericValue>> entry: features.entrySet()) { List<GenericValue> currentFeatures = entry.getValue(); List<Map<String, Object>> newCombinations = new LinkedList<Map<String,Object>>(); List<Map<String, Object>> combinations; // start with either existing combinations or from scratch if (oldCombinations.size() > 0) { combinations = oldCombinations; } else { combinations = new LinkedList<Map<String,Object>>(); } // in both cases, use each feature of current feature type's idCode and // product feature and add it to the id code and product feature applications // of the next variant. just a matter of whether we're starting with an // existing list of features and id code or from scratch. if (combinations.size()==0) { for (GenericValue currentFeature: currentFeatures) { if (currentFeature.getString("productFeatureApplTypeId").equals("SELECTABLE_FEATURE")) { Map<String, Object> newCombination = new HashMap<String, Object>(); List<GenericValue> newFeatures = new LinkedList<GenericValue>(); List<String> newFeatureIds = new LinkedList<String>(); if (currentFeature.getString("idCode") != null) { newCombination.put("defaultVariantProductId", productId + currentFeature.getString("idCode")); } else { newCombination.put("defaultVariantProductId", productId); } newFeatures.add(currentFeature); newFeatureIds.add(currentFeature.getString("productFeatureId")); newCombination.put("curProductFeatureAndAppls", newFeatures); newCombination.put("curProductFeatureIds", newFeatureIds); newCombinations.add(newCombination); } } } else { for (Map<String, Object> combination: combinations) { for (GenericValue currentFeature: currentFeatures) { if (currentFeature.getString("productFeatureApplTypeId").equals("SELECTABLE_FEATURE")) { Map<String, Object> newCombination = new HashMap<String, Object>(); // .clone() is important, or you'll keep adding to the same List for all the variants // have to cast twice: once from get() and once from clone() List<GenericValue> newFeatures = UtilMisc.makeListWritable(UtilGenerics.<GenericValue>checkList(combination.get("curProductFeatureAndAppls"))); List<String> newFeatureIds = UtilMisc.makeListWritable(UtilGenerics.<String>checkList(combination.get("curProductFeatureIds"))); if (currentFeature.getString("idCode") != null) { newCombination.put("defaultVariantProductId", combination.get("defaultVariantProductId") + currentFeature.getString("idCode")); } else { newCombination.put("defaultVariantProductId", combination.get("defaultVariantProductId")); } newFeatures.add(currentFeature); newFeatureIds.add(currentFeature.getString("productFeatureId")); newCombination.put("curProductFeatureAndAppls", newFeatures); newCombination.put("curProductFeatureIds", newFeatureIds); newCombinations.add(newCombination); } } } } if (newCombinations.size() >= oldCombinations.size()) { oldCombinations = newCombinations; // save the newly expanded list as oldCombinations } } int defaultCodeCounter = 1; Set<String> defaultVariantProductIds = new HashSet<String>(); // this map will contain the codes already used (as keys) defaultVariantProductIds.add(productId); // now figure out which of these combinations already have productIds associated with them for (Map<String, Object> combination: oldCombinations) { // Verify if the default code is already used, if so add a numeric suffix if (defaultVariantProductIds.contains(combination.get("defaultVariantProductId"))) { combination.put("defaultVariantProductId", combination.get("defaultVariantProductId") + "-" + (defaultCodeCounter < 10? "0" + defaultCodeCounter: "" + defaultCodeCounter)); defaultCodeCounter++; } defaultVariantProductIds.add((String) combination.get("defaultVariantProductId")); results = dispatcher.runSync("getAllExistingVariants", UtilMisc.toMap("productId", productId, "productFeatureAppls", combination.get("curProductFeatureIds"))); combination.put("existingVariantProductIds", results.get("variantProductIds")); } results = ServiceUtil.returnSuccess(); results.put("featureCombinations", oldCombinations); } catch (GenericServiceException ex) { Debug.logError(ex, ex.getMessage(), module); return ServiceUtil.returnError(ex.getMessage()); } return results; } /* * Parameters: productCategoryId (String) and productFeatures (a List of ProductFeature GenericValues) * Result: products (a List of Product GenericValues) */ public static Map<String, Object> getCategoryVariantProducts(DispatchContext dctx, Map<String, ? extends Object> context) { Map<String, Object> results = new HashMap<String, Object>(); LocalDispatcher dispatcher = dctx.getDispatcher(); List<GenericValue> productFeatures = UtilGenerics.checkList(context.get("productFeatures")); String productCategoryId = (String) context.get("productCategoryId"); Locale locale = (Locale) context.get("locale"); // get all the product members of the product category Map<String, Object> result; try { result = dispatcher.runSync("getProductCategoryMembers", UtilMisc.toMap("categoryId", productCategoryId)); } catch (GenericServiceException ex) { Debug.logError("Cannot get category memebers for " + productCategoryId + " due to error: " + ex.getMessage(), module); return ServiceUtil.returnError(ex.getMessage()); } List<GenericValue> memberProducts = UtilGenerics.checkList(result.get("categoryMembers")); if ((memberProducts != null) && (memberProducts.size() > 0)) { // construct a Map of productFeatureTypeId -> productFeatureId from the productFeatures List Map<String, String> featuresByType = new HashMap<String, String>(); for (GenericValue nextFeature: productFeatures) { featuresByType.put(nextFeature.getString("productFeatureTypeId"), nextFeature.getString("productFeatureId")); } List<GenericValue> products = new LinkedList<GenericValue>(); // final list of variant products for (GenericValue memberProduct: memberProducts) { // find variants for each member product of the category try { result = dispatcher.runSync("getProductVariant", UtilMisc.toMap("productId", memberProduct.getString("productId"), "selectedFeatures", featuresByType)); } catch (GenericServiceException ex) { Debug.logError("Cannot get product variants for " + memberProduct.getString("productId") + " due to error: " + ex.getMessage(), module); return ServiceUtil.returnError(ex.getMessage()); } List<GenericValue> variantProducts = UtilGenerics.checkList(result.get("products")); if ((variantProducts != null) && (variantProducts.size() > 0)) { products.addAll(variantProducts); } else { Debug.logWarning("Product " + memberProduct.getString("productId") + " did not have any variants for the given features", module); } } if (products.size() == 0) { return ServiceUtil.returnError(UtilProperties.getMessage(resource, "ProductCategoryNoVariants", locale)); } else { results = ServiceUtil.returnSuccess(); results.put("products", products); } } else { Debug.logWarning("No products found in " + productCategoryId, module); } return results; } }