/*
* #%L
* BroadleafCommerce CMS Module
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* 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.
* #L%
*/
package org.broadleafcommerce.cms.structure.service;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.cms.field.domain.FieldDefinition;
import org.broadleafcommerce.cms.field.domain.FieldGroup;
import org.broadleafcommerce.cms.file.service.StaticAssetService;
import org.broadleafcommerce.cms.structure.dao.StructuredContentDao;
import org.broadleafcommerce.cms.structure.domain.StructuredContent;
import org.broadleafcommerce.cms.structure.domain.StructuredContentField;
import org.broadleafcommerce.cms.structure.domain.StructuredContentItemCriteria;
import org.broadleafcommerce.cms.structure.domain.StructuredContentRule;
import org.broadleafcommerce.cms.structure.domain.StructuredContentType;
import org.broadleafcommerce.common.cache.CacheStatType;
import org.broadleafcommerce.common.cache.StatisticsService;
import org.broadleafcommerce.common.extensibility.jpa.SiteDiscriminator;
import org.broadleafcommerce.common.extension.ExtensionResultHolder;
import org.broadleafcommerce.common.file.service.StaticAssetPathService;
import org.broadleafcommerce.common.locale.domain.Locale;
import org.broadleafcommerce.common.locale.service.LocaleService;
import org.broadleafcommerce.common.locale.util.LocaleUtil;
import org.broadleafcommerce.common.money.Money;
import org.broadleafcommerce.common.persistence.EntityConfiguration;
import org.broadleafcommerce.common.rule.RuleProcessor;
import org.broadleafcommerce.common.sandbox.domain.SandBox;
import org.broadleafcommerce.common.structure.dto.ItemCriteriaDTO;
import org.broadleafcommerce.common.structure.dto.StructuredContentDTO;
import org.broadleafcommerce.common.util.FormatUtil;
import org.broadleafcommerce.common.web.BroadleafRequestContext;
import org.hibernate.Criteria;
import org.hibernate.criterion.Projections;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
/**
* @author bpolster
*/
@Service("blStructuredContentService")
public class StructuredContentServiceImpl implements StructuredContentService {
protected static final Log LOG = LogFactory.getLog(StructuredContentServiceImpl.class);
protected static String AND = " && ";
protected static final String FOREIGN_LOOKUP = "BLC_FOREIGN_LOOKUP";
@Resource(name="blStructuredContentDao")
protected StructuredContentDao structuredContentDao;
@Resource(name="blStaticAssetService")
protected StaticAssetService staticAssetService;
@Resource(name="blStaticAssetPathService")
protected StaticAssetPathService staticAssetPathService;
@Resource(name="blLocaleService")
protected LocaleService localeService;
@Resource(name="blContentRuleProcessors")
protected List<RuleProcessor<StructuredContentDTO>> contentRuleProcessors;
@Resource(name="blEntityConfiguration")
protected EntityConfiguration entityConfiguration;
@Resource(name = "blStructuredContentServiceExtensionManager")
protected StructuredContentServiceExtensionManager extensionManager;
@Resource(name="blStatisticsService")
protected StatisticsService statisticsService;
protected Cache structuredContentCache;
@Override
public StructuredContent findStructuredContentById(Long contentId) {
return structuredContentDao.findStructuredContentById(contentId);
}
@Override
public StructuredContentType findStructuredContentTypeById(Long id) {
return structuredContentDao.findStructuredContentTypeById(id);
}
@Override
public StructuredContentType findStructuredContentTypeByName(String name) {
return structuredContentDao.findStructuredContentTypeByName(name);
}
@Override
public List<StructuredContentType> retrieveAllStructuredContentTypes() {
return structuredContentDao.retrieveAllStructuredContentTypes();
}
@Override
public List<StructuredContent> findContentItems(Criteria c) {
return c.list();
}
@Override
public List<StructuredContent> findAllContentItems() {
return structuredContentDao.findAllContentItems();
}
@Override
public Long countContentItems(Criteria c) {
c.setProjection(Projections.rowCount());
return (Long) c.uniqueResult();
}
/**
* Saves the given <b>type</b> and returns the merged instance
*/
@Override
public StructuredContentType saveStructuredContentType(StructuredContentType type) {
return structuredContentDao.saveStructuredContentType(type);
}
/**
* Converts a list of structured content items to a list of structured content DTOs.<br>
* Internally calls buildStructuredContentDTO(...).
*
* @param structuredContentList
* @param secure
* @return
* @see {@link #buildStructuredContentDTO(StructuredContent, boolean)}
*/
@Override
public List<StructuredContentDTO> buildStructuredContentDTOList(List<StructuredContent> structuredContentList, boolean secure) {
List<StructuredContentDTO> dtoList = new ArrayList<StructuredContentDTO>();
if (structuredContentList != null) {
for(StructuredContent sc : structuredContentList) {
dtoList.add(buildStructuredContentDTO(sc, secure));
}
}
return dtoList;
}
@Override
public List<StructuredContentDTO> evaluateAndPriortizeContent(List<StructuredContentDTO> structuredContentList, int count, Map<String, Object> ruleDTOs) {
// some optimization for single item lists which don't require prioritization
if (structuredContentList.size() == 1) {
if (processContentRules(structuredContentList.get(0), ruleDTOs)) {
return structuredContentList;
} else {
return new ArrayList<StructuredContentDTO>();
}
}
ExtensionResultHolder resultHolder = new ExtensionResultHolder();
extensionManager.getProxy().modifyStructuredContentDtoList(structuredContentList, resultHolder);
if (resultHolder.getResult() != null) {
structuredContentList = (List<StructuredContentDTO>) resultHolder.getResult();
}
Iterator<StructuredContentDTO> structuredContentIterator = structuredContentList.iterator();
List<StructuredContentDTO> returnList = new ArrayList<StructuredContentDTO>();
List<StructuredContentDTO> tmpList = new ArrayList<StructuredContentDTO>();
Integer lastPriority = Integer.MIN_VALUE;
while (structuredContentIterator.hasNext()) {
StructuredContentDTO sc = structuredContentIterator.next();
if (! lastPriority.equals(sc.getPriority())) {
// If we've moved to another priority, then shuffle all of the items
// with the previous priority and add them to the return list.
if (tmpList.size() > 1) {
Collections.shuffle(tmpList);
}
returnList.addAll(tmpList);
tmpList.clear();
// If we've added enough items to satisfy the count, then return the
// list.
if (returnList.size() == count) {
return returnList;
} else if (returnList.size() > count) {
return returnList.subList(0, count);
} else {
if (processContentRules(sc, ruleDTOs)) {
tmpList.add(sc);
}
}
} else {
if (processContentRules(sc, ruleDTOs)) {
tmpList.add(sc);
}
}
lastPriority = sc.getPriority();
}
if (tmpList.size() > 1) {
Collections.shuffle(tmpList);
}
returnList.addAll(tmpList);
if (returnList.size() > count) {
return returnList.subList(0, count);
}
return returnList;
}
@Override
public List<StructuredContentDTO> lookupStructuredContentItemsByType(StructuredContentType contentType, Locale locale,
Integer count, Map<String, Object> ruleDTOs, boolean secure) {
List<StructuredContentDTO> contentDTOList = null;
Locale languageOnlyLocale = findLanguageOnlyLocale(locale);
BroadleafRequestContext context = BroadleafRequestContext.getBroadleafRequestContext();
Long site = context.getSite() == null?null:context.getSite().getId();
String cacheKey = buildTypeKey(context.getSandBox(), site, languageOnlyLocale, contentType.getName());
cacheKey = cacheKey+"-"+secure;
if (context.isProductionSandBox()) {
contentDTOList = getStructuredContentListFromCache(cacheKey);
}
if (contentDTOList == null) {
List<StructuredContent> contentList = structuredContentDao.findActiveStructuredContentByType(contentType,
locale, languageOnlyLocale);
contentDTOList = buildStructuredContentDTOList(contentList, secure);
if (context.isProductionSandBox()) {
addStructuredContentListToCache(cacheKey, contentDTOList);
}
}
return evaluateAndPriortizeContent(contentDTOList, count, ruleDTOs);
}
@Override
public List<StructuredContentDTO> lookupStructuredContentItemsByName(StructuredContentType contentType,
String contentName, org.broadleafcommerce.common.locale.domain.Locale locale,
Integer count, Map<String, Object> ruleDTOs, boolean secure) {
List<StructuredContentDTO> contentDTOList = null;
Locale languageOnlyLocale = findLanguageOnlyLocale(locale);
BroadleafRequestContext context = BroadleafRequestContext.getBroadleafRequestContext();
Long site = context.getSite() == null?null:context.getSite().getId();
String cacheKey = buildNameKey(context.getSandBox(), site, languageOnlyLocale, contentType.getName(), contentName);
cacheKey = cacheKey+"-"+secure;
if (context.isProductionSandBox()) {
contentDTOList = getStructuredContentListFromCache(cacheKey);
}
if (contentDTOList == null) {
List<StructuredContent> productionContentList = structuredContentDao.findActiveStructuredContentByNameAndType(
contentType, contentName, locale, languageOnlyLocale);
contentDTOList = buildStructuredContentDTOList(productionContentList, secure);
if (context.isProductionSandBox()) {
addStructuredContentListToCache(cacheKey, contentDTOList);
}
}
return evaluateAndPriortizeContent(contentDTOList, count, ruleDTOs);
}
@Override
public List<StructuredContentDTO> convertToDtos(List<StructuredContent> scs, boolean isSecure) {
List<StructuredContentDTO> contentDTOList = new ArrayList<StructuredContentDTO>();
for (StructuredContent sc : scs) {
String cacheKey = "SC|" + sc.getId();
StructuredContentDTO dto = null;
BroadleafRequestContext context = BroadleafRequestContext.getBroadleafRequestContext();
if (context.isProductionSandBox()) {
dto = getSingleStructuredContentFromCache(cacheKey);
}
if (dto == null) {
dto = buildStructuredContentDTO(sc, isSecure);
if (context.isProductionSandBox()) {
addSingleStructuredContentToCache(cacheKey, dto);
}
}
contentDTOList.add(dto);
}
return contentDTOList;
}
@Override
public List<StructuredContentDTO> lookupStructuredContentItemsByName(String contentName,
org.broadleafcommerce.common.locale.domain.Locale locale,
Integer count, Map<String, Object> ruleDTOs, boolean secure) {
List<StructuredContentDTO> contentDTOList = null;
Locale languageOnlyLocale = findLanguageOnlyLocale(locale);
BroadleafRequestContext context = BroadleafRequestContext.getBroadleafRequestContext();
Long site = context.getSite() == null?null:context.getSite().getId();
String cacheKey = buildNameKey(context.getSandBox(), site, languageOnlyLocale, "any", contentName);
cacheKey = cacheKey+"-"+secure;
if (context.isProductionSandBox()) {
contentDTOList = getStructuredContentListFromCache(cacheKey);
}
if (contentDTOList == null) {
List<StructuredContent> productionContentList = structuredContentDao.findActiveStructuredContentByName(contentName, locale, languageOnlyLocale);
contentDTOList = buildStructuredContentDTOList(productionContentList, secure);
if (context.isProductionSandBox()) {
addStructuredContentListToCache(cacheKey, contentDTOList);
}
}
return evaluateAndPriortizeContent(contentDTOList, count, ruleDTOs);
}
public List<RuleProcessor<StructuredContentDTO>> getContentRuleProcessors() {
return contentRuleProcessors;
}
public void setContentRuleProcessors(List<RuleProcessor<StructuredContentDTO>> contentRuleProcessors) {
this.contentRuleProcessors = contentRuleProcessors;
}
/**
* Call to evict an item from the cache.
* @param sc
*/
@Override
public void removeStructuredContentFromCache(SandBox sandBox, StructuredContent sc) {
// Remove secure and non-secure instances of the page.
// Typically the page will be in one or the other if at all.
removeItemFromCache(buildNameKey(sandBox, sc), buildTypeKey(sandBox, sc));
}
@Override
public Locale findLanguageOnlyLocale(Locale locale) {
if (locale != null ) {
Locale languageOnlyLocale = localeService.findLocaleByCode(LocaleUtil.findLanguageCode(locale));
if (languageOnlyLocale != null) {
return languageOnlyLocale;
}
}
return locale;
}
@Override
public Cache getStructuredContentCache() {
if (structuredContentCache == null) {
structuredContentCache = CacheManager.getInstance().getCache("cmsStructuredContentCache");
}
return structuredContentCache;
}
/**
* Call to evict both secure and non-secure SC items matching
* the passed in key.
*
* @param nameKey
*/
@Override
public void removeItemFromCache(String nameKey, String typeKey) {
// Remove secure and non-secure instances of the structured content.
// Typically the structured content will be in one or the other if at all.
if (!StringUtils.isEmpty(nameKey)) {
getStructuredContentCache().remove(nameKey+"-"+true);
getStructuredContentCache().remove(nameKey+"-"+false);
}
if (!StringUtils.isEmpty(typeKey)) {
getStructuredContentCache().remove(typeKey+"-"+true);
getStructuredContentCache().remove(typeKey+"-"+false);
}
}
protected String buildRuleExpression(StructuredContent sc) {
StringBuffer ruleExpression = null;
Map<String, StructuredContentRule> ruleMap = sc.getStructuredContentMatchRules();
if (ruleMap != null) {
for (String ruleKey : ruleMap.keySet()) {
if (ruleMap.get(ruleKey).getMatchRule() == null) {
continue;
}
if (ruleExpression == null) {
ruleExpression = new StringBuffer(ruleMap.get(ruleKey).getMatchRule());
} else {
ruleExpression.append(AND);
ruleExpression.append(ruleMap.get(ruleKey).getMatchRule());
}
}
}
if (ruleExpression != null) {
return ruleExpression.toString();
} else {
return null;
}
}
protected List<ItemCriteriaDTO> buildItemCriteriaDTOList(StructuredContent sc) {
List<ItemCriteriaDTO> itemCriteriaDTOList = new ArrayList<ItemCriteriaDTO>();
for(StructuredContentItemCriteria criteria : sc.getQualifyingItemCriteria()) {
ItemCriteriaDTO criteriaDTO = entityConfiguration.createEntityInstance(ItemCriteriaDTO.class.getName(), ItemCriteriaDTO.class);
criteriaDTO.setMatchRule(criteria.getMatchRule());
criteriaDTO.setQty(criteria.getQuantity());
itemCriteriaDTOList.add(criteriaDTO);
}
return itemCriteriaDTOList;
}
/**
* Parses the given {@link StructuredContent} into its {@link StructuredContentDTO} representation. This will also
* format the values from {@link StructuredContentDTO#getValues()} into their actual data types. For instance, if the
* given {@link StructuredContent} has a DATE field, then this method will ensure that the resulting object in the values
* map of the DTO is a {@link Date} rather than just a String representing a date.
*
* Current support of parsing field types is:
* DATE - {@link Date}
* BOOLEAN - {@link Boolean}
* DECIMAL - {@link BigDecimal}
* INTEGER - {@link Integer}
* MONEY - {@link Money}
*
* All other fields are treated as strings. This will also fix URL strings that have the CMS prefix (like images) by
* prepending the standard CMS prefix with the particular environment prefix
*
* @param sc
* @param scDTO
* @param secure
* @see {@link StaticAssetService#getStaticAssetEnvironmentUrlPrefix()}
*/
protected void buildFieldValues(StructuredContent sc, StructuredContentDTO scDTO, boolean secure) {
String cmsPrefix = staticAssetPathService.getStaticAssetUrlPrefix();
scDTO.getValues().put("id", sc.getId());
for (String fieldKey : sc.getStructuredContentFieldXrefs().keySet()) {
StructuredContentField scf = sc.getStructuredContentFieldXrefs().get(fieldKey).getStructuredContentField();
String originalValue = scf.getValue();
if (StringUtils.isNotBlank(originalValue) && StringUtils.isNotBlank(cmsPrefix) && originalValue.contains(cmsPrefix)) {
//This may either be an ASSET_LOOKUP image path or an HTML block (with multiple <img>) or a plain STRING that contains the cmsPrefix.
//If there is an environment prefix configured (e.g. a CDN), then we must replace the cmsPrefix with this one.
String fldValue = staticAssetPathService.convertAllAssetPathsInContent(originalValue, secure);
scDTO.getValues().put(fieldKey, fldValue);
} else {
FieldDefinition definition = null;
Iterator<FieldGroup> groupIterator = sc.getStructuredContentType().getStructuredContentFieldTemplate().getFieldGroups().iterator();
while (groupIterator.hasNext() && definition == null) {
FieldGroup group = groupIterator.next();
for (FieldDefinition def : group.getFieldDefinitions()) {
if (def.getName().equals(fieldKey)) {
definition = def;
break;
}
}
}
if (definition != null) {
Object value = null;
if (originalValue != null) {
switch (definition.getFieldType()) {
case DATE:
try {
value = FormatUtil.getDateFormat().parse(originalValue);
} catch (Exception e) {
value = originalValue;
}
break;
case BOOLEAN:
value = new Boolean(originalValue);
break;
case DECIMAL:
value = new BigDecimal(originalValue);
break;
case INTEGER:
value = Integer.parseInt(originalValue);
break;
case MONEY:
value = new Money(originalValue);
break;
case ADDITIONAL_FOREIGN_KEY:
value = FOREIGN_LOOKUP + '|' + definition.getAdditionalForeignKeyClass() + '|' + originalValue;
break;
default:
value = originalValue;
break;
}
}
scDTO.getValues().put(fieldKey, value);
} else {
scDTO.getValues().put(fieldKey, sc.getStructuredContentFieldXrefs().get(fieldKey).getStructuredContentField().getValue());
}
}
}
// allow modules to contribute to the fields of the DTO
extensionManager.getProxy().populateAdditionalStructuredContentFields(sc, scDTO, secure);
}
/**
* Converts a StructuredContent into a StructuredContentDTO. If the item contains fields with
* broadleaf cms urls, the urls are converted to utilize the domain.
*
* The StructuredContentDTO is built via the {@link EntityConfiguration}. To override the actual type that is returned,
* include an override in an applicationContext like any other entity override.
*
* @param sc
* @param secure
* @return
*/
@Override
public StructuredContentDTO buildStructuredContentDTO(StructuredContent sc, boolean secure) {
StructuredContentDTO scDTO = entityConfiguration.createEntityInstance(StructuredContentDTO.class.getName(), StructuredContentDTO.class);
scDTO.setContentName(sc.getContentName());
scDTO.setContentType(sc.getStructuredContentType().getName());
scDTO.setId(sc.getId());
scDTO.setPriority(sc.getPriority());
if (sc.getLocale() != null) {
scDTO.setLocaleCode(sc.getLocale().getLocaleCode());
}
scDTO.setRuleExpression(buildRuleExpression(sc));
buildFieldValues(sc, scDTO, secure);
if (sc.getQualifyingItemCriteria() != null && sc.getQualifyingItemCriteria().size() > 0) {
scDTO.setItemCriteriaDTOList(buildItemCriteriaDTOList(sc));
}
return scDTO;
}
protected boolean processContentRules(StructuredContentDTO sc, Map<String, Object> ruleDTOs) {
if (contentRuleProcessors != null) {
for (RuleProcessor<StructuredContentDTO> processor : contentRuleProcessors) {
boolean matchFound = processor.checkForMatch(sc, ruleDTOs);
if (! matchFound) {
return false;
}
}
}
return true;
}
@Override
public String buildTypeKey(SandBox currentSandbox, Long site, Locale locale, String contentType) {
StringBuilder key = new StringBuilder(contentType);
if (locale != null) {
key.append("-").append(locale.getLocaleCode());
}
if (currentSandbox != null) {
key.append("-").append(currentSandbox.getId());
}
if (site != null) {
key.append("-").append(site);
}
return key.toString();
}
protected String buildNameKey(SandBox sandBox, StructuredContent sc) {
Long site = (sc instanceof SiteDiscriminator)?((SiteDiscriminator) sc).getSiteDiscriminator():null;
return buildNameKey(sandBox, site, findLanguageOnlyLocale(sc.getLocale()), sc.getStructuredContentType().getName(), sc.getContentName());
}
protected String buildTypeKey(SandBox sandBox, StructuredContent sc) {
Long site = (sc instanceof SiteDiscriminator)?((SiteDiscriminator) sc).getSiteDiscriminator():null;
return buildTypeKey(sandBox, site, findLanguageOnlyLocale(sc.getLocale()), sc.getStructuredContentType().getName());
}
protected String buildNameKey(SandBox currentSandbox, Long site, Locale locale, String contentType, String contentName) {
StringBuffer key = new StringBuffer(contentType).append("-").append(contentName);
if (locale != null) {
key.append("-").append(locale.getLocaleCode());
}
if (currentSandbox != null) {
key.append("-").append(currentSandbox.getId());
}
if (site != null) {
key.append("-").append(site);
}
return key.toString();
}
@Override
public void addStructuredContentListToCache(String key, List<StructuredContentDTO> scDTOList) {
getStructuredContentCache().put(new Element(key, scDTOList));
}
protected void addSingleStructuredContentToCache(String key, StructuredContentDTO scDTO) {
getStructuredContentCache().put(new Element(key, scDTO));
}
protected StructuredContentDTO getSingleStructuredContentFromCache(String key) {
Element scElement = getStructuredContentCache().get(key);
if (scElement != null) {
statisticsService.addCacheStat(CacheStatType.STRUCTURED_CONTENT_CACHE_HIT_RATE.toString(), true);
return (StructuredContentDTO) scElement.getValue();
}
statisticsService.addCacheStat(CacheStatType.STRUCTURED_CONTENT_CACHE_HIT_RATE.toString(), false);
return null;
}
@Override
public List<StructuredContentDTO> getStructuredContentListFromCache(String key) {
Element scElement = getStructuredContentCache().get(key);
if (scElement != null) {
statisticsService.addCacheStat(CacheStatType.STRUCTURED_CONTENT_CACHE_HIT_RATE.toString(), true);
return (List<StructuredContentDTO>) scElement.getValue();
}
statisticsService.addCacheStat(CacheStatType.STRUCTURED_CONTENT_CACHE_HIT_RATE.toString(), false);
return null;
}
}