/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.services.ads;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.access.OperatorPermission;
import nl.strohalm.cyclos.dao.ads.AdDAO;
import nl.strohalm.cyclos.entities.IndexOperation;
import nl.strohalm.cyclos.entities.IndexOperation.EntityType;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.ads.AbstractAdQuery;
import nl.strohalm.cyclos.entities.ads.Ad;
import nl.strohalm.cyclos.entities.ads.Ad.Status;
import nl.strohalm.cyclos.entities.ads.Ad.TradeType;
import nl.strohalm.cyclos.entities.ads.AdCategory;
import nl.strohalm.cyclos.entities.ads.AdCategoryWithCounterQuery;
import nl.strohalm.cyclos.entities.ads.AdCategoryWithCounterVO;
import nl.strohalm.cyclos.entities.ads.AdQuery;
import nl.strohalm.cyclos.entities.ads.FullTextAdQuery;
import nl.strohalm.cyclos.entities.customization.fields.AdCustomField;
import nl.strohalm.cyclos.entities.customization.fields.AdCustomFieldValue;
import nl.strohalm.cyclos.entities.customization.fields.MemberCustomField;
import nl.strohalm.cyclos.entities.exceptions.DaoException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.GroupFilter;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings.ExternalAdPublication;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.Operator;
import nl.strohalm.cyclos.services.customization.AdCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.customization.MemberCustomFieldServiceLocal;
import nl.strohalm.cyclos.services.elements.MemberServiceLocal;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.CustomFieldHelper;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.RangeConstraint;
import nl.strohalm.cyclos.utils.StringHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.cache.Cache;
import nl.strohalm.cyclos.utils.cache.CacheCallback;
import nl.strohalm.cyclos.utils.cache.CacheManager;
import nl.strohalm.cyclos.utils.conversion.Transformer;
import nl.strohalm.cyclos.utils.lucene.IndexOperationListener;
import nl.strohalm.cyclos.utils.lucene.IndexOperationRunner;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.PageHelper;
import nl.strohalm.cyclos.utils.query.QueryParameters.ResultType;
import nl.strohalm.cyclos.utils.validation.DelegatingValidator;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.InvalidError;
import nl.strohalm.cyclos.utils.validation.LengthValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredValidation;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.webservices.ads.AdResultPage;
import nl.strohalm.cyclos.webservices.ads.FullTextAdSearchParameters;
import nl.strohalm.cyclos.webservices.model.AdVO;
import nl.strohalm.cyclos.webservices.model.GeneralAdVO;
import nl.strohalm.cyclos.webservices.model.MyAdVO;
import nl.strohalm.cyclos.webservices.utils.AdHelper;
import nl.strohalm.cyclos.webservices.utils.QueryHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
/**
* Implementation class for the Advertisement service interface
* @author rafael
* @author luis
*/
public class AdServiceImpl implements AdServiceLocal {
/**
* Validates max description size
* @author Jefferson Magno
*/
public class MaxAdDescriptionSizeValidation implements PropertyValidation {
private static final long serialVersionUID = -5580051445666373995L;
@Override
public ValidationError validate(final Object object, final Object name, final Object value) {
final Ad ad = (Ad) object;
Element element = fetchService.fetch(ad.getOwner(), Element.Relationships.GROUP);
if (element == null) {
if (!LoggedUser.hasUser()) {
return null;
}
element = LoggedUser.element();
}
final Group group = element.getGroup();
if (group instanceof MemberGroup) {
final MemberGroup memberGroup = fetchService.fetch((MemberGroup) group);
final int maxAdDescriptionSize = memberGroup.getMemberSettings().getMaxAdDescriptionSize();
String description = value == null ? null : value.toString();
if (ad.isHtml()) {
description = StringHelper.removeMarkupTagsAndUnescapeEntities(description);
}
return new LengthValidation(RangeConstraint.to(maxAdDescriptionSize)).validate(object, name, description);
}
return null;
}
}
/**
* Validates max publication period
* @author luis
*/
public class MaxPublicationTimeValidation implements GeneralValidation {
private static final long serialVersionUID = 1616929350799341483L;
@Override
public ValidationError validate(final Object object) {
final Ad ad = (Ad) object;
Element element = fetchService.fetch(ad.getOwner(), Element.Relationships.GROUP);
if (element == null) {
if (!LoggedUser.hasUser()) {
return null;
}
element = LoggedUser.element();
}
final Group group = fetchService.fetch(element.getGroup());
Calendar begin = null;
Calendar end = null;
try {
begin = ad.getPublicationPeriod().getBegin();
end = ad.getPublicationPeriod().getEnd();
} catch (final NullPointerException e) {
}
// Check max publication time
if (begin != null && end != null && !ad.isPermanent() && (group instanceof MemberGroup)) {
final TimePeriod maxAdPublicationTime = ((MemberGroup) group).getMemberSettings().getMaxAdPublicationTime();
final Calendar maxEnd = maxAdPublicationTime.add(begin);
if (end.after(maxEnd)) {
return new ValidationError("ad.error.maxPublicationTimeExceeded");
}
}
return null;
}
}
/**
* Validates an ad publication period
* @author luis
*/
public class PublicationPeriodValidation implements PropertyValidation {
private static final long serialVersionUID = -6352683891570105522L;
@Override
public ValidationError validate(final Object object, final Object name, final Object value) {
final Ad ad = (Ad) object;
if (!ad.isPermanent()) {
final ValidationError required = RequiredValidation.instance().validate(object, name, value);
if (required != null) {
return required;
}
if (name.toString().endsWith(".end") && ad.getPublicationPeriod() != null) {
final Calendar beginDate = ad.getPublicationPeriod().getBegin();
final Calendar endDate = (Calendar) value;
// Check if end is after begin
if (beginDate != null && endDate != null && !endDate.after(beginDate)) {
return new InvalidError();
}
}
}
return null;
}
}
private AdDAO adDao;
private AdCustomFieldServiceLocal adCustomFieldService;
private FetchServiceLocal fetchService;
private AdCategoryServiceLocal adCategoryService;
private MemberNotificationHandler memberNotificationHandler;
private PermissionServiceLocal permissionService;
private SettingsServiceLocal settingsService;
private CacheManager cacheManager;
private AdHelper adHelper;
private MemberCustomFieldServiceLocal memberCustomFieldServiceLocal;
private QueryHelper queryHelper;
private MemberServiceLocal memberServiceLocal;
private CustomFieldHelper customFieldHelper;
@Override
public int countExternalAds(final Long adCategoryId, final TradeType type) {
final AdQuery query = new AdQuery();
query.setStatus(Ad.Status.ACTIVE);
query.setTradeType(type);
query.setCategory(EntityHelper.reference(AdCategory.class, adCategoryId));
query.setExternalPublication(true);
query.setPageForCount();
return PageHelper.getTotalCount(search(query));
}
@Override
public Integer countMembersWithAds(final Collection<MemberGroup> memberGroups, final Calendar timePoint) {
return adDao.getNumberOfMembersWithAds(timePoint, memberGroups);
}
@Override
public List<Ad> fullTextSearch(final FullTextAdQuery query) throws DaoException {
if (query.getCategory() != null && !adCategoryService.getActiveCategoriesId().contains(query.getCategory().getId())) {
return Collections.emptyList();
}
if (query.getCategory() == null) {
query.setCategoriesIds(adCategoryService.getActiveCategoriesId());
} else {
query.setCategoriesIds(new LinkedList<Long>());
query.getCategoriesIds().add(query.getCategory().getId());
}
setupGroupFilters(query);
if (!applyLoggedUserFilters(query)) {
return Collections.emptyList();
}
query.setAnalyzer(settingsService.getLocalSettings().getLanguage().getAnalyzer());
return adDao.fullTextSearch(query);
}
@Override
public AdResultPage getAdResultPage(final FullTextAdSearchParameters params, final String memberPrincipal) {
FullTextAdQuery query = adHelper.toFullTextQuery(params);
// The memberPrincipal doesn't exist on the original FullTextAdSearchParameters, and must be handled here
if (query.getOwner() == null && StringUtils.isNotEmpty(memberPrincipal)) {
query.setOwner(memberServiceLocal.loadByIdOrPrincipal(null, null, memberPrincipal));
}
// Execute the search
List<Ad> ads = fullTextSearch(query);
return queryHelper.toResultPage(AdResultPage.class, ads, new Transformer<Ad, AdVO>() {
@Override
public AdVO transform(final Ad ad) {
return getAdVO(GeneralAdVO.class, ad, params.getShowAdFields(), params.getShowMemberFields(), true);
}
});
}
@Override
public <VO extends AdVO> VO getAdVO(final Class<VO> voType, final Ad ad, final boolean useAdFields, final boolean useMemberFields, final boolean onlyForAdSearchMemberFields) {
final List<AdCustomField> allAdFields = useAdFields ? adCustomFieldService.list() : null;
List<MemberCustomField> memberFields = null;
if (useMemberFields) {
if (onlyForAdSearchMemberFields) {
memberFields = customFieldHelper.onlyForAdSearch(memberCustomFieldServiceLocal.list());
} else {
memberFields = memberCustomFieldServiceLocal.list();
if (!LoggedUser.isUnrestrictedClient()) {
MemberGroup group = LoggedUser.member().getMemberGroup();
memberFields = customFieldHelper.onlyVisibleFields(memberFields, group);
}
}
}
return adHelper.toVO(voType, ad, allAdFields, memberFields);
}
@Override
public List<AdCategoryWithCounterVO> getCategoriesWithCounters(final AdCategoryWithCounterQuery query) {
return getCountersCache().get(query, new CacheCallback() {
@Override
public Object retrieve() {
List<AdCategory> categories = adCategoryService.listRoot();
return adDao.getCategoriesWithCounters(categories, query);
}
});
}
@Override
public MyAdVO getMyVO(final Ad ad) {
return adHelper.toMyVO(ad);
}
@Override
public Ad getNextAdToNotify() {
AdQuery query = new AdQuery();
query.setUniqueResult();
query.setStatus(Status.ACTIVE);
query.setMembersNotified(false);
query.setSkipOrder(true);
List<Ad> list = search(query);
Iterator<Ad> iterator = list.iterator();
return iterator.hasNext() ? iterator.next() : null;
}
@Override
public Map<Ad.Status, Integer> getNumberOfAds(final Calendar date, final Member member) {
final Map<Ad.Status, Integer> numberOfAds = new EnumMap<Ad.Status, Integer>(Ad.Status.class);
final AdQuery query = new AdQuery();
query.setOwner(member);
query.setPageForCount();
// date is for history
if (date != null) {
query.setHistoryDate(date);
query.setIncludeDeleted(true);
}
for (final Ad.Status status : Ad.Status.values()) {
query.setStatus(status);
final int totalCount = PageHelper.getTotalCount(search(query));
numberOfAds.put(status, totalCount);
}
return numberOfAds;
}
@Override
public int getNumberOfAds(final Collection<MemberGroup> memberGroups, final Status status, final Calendar timePoint) {
return adDao.getNumberOfAds(timePoint, memberGroups, status);
}
@Override
public void invalidateCountersCache() {
// Invalidate the counters cache
if (settingsService.getLocalSettings().isShowCountersInAdCategories()) {
getCountersCache().clear();
}
}
@Override
public boolean isEditable(final Ad ad) {
return permissionService.permission(ad.getOwner())
.admin(AdminMemberPermission.ADS_MANAGE)
.broker(BrokerPermission.ADS_MANAGE)
.member(MemberPermission.ADS_PUBLISH)
.operator(OperatorPermission.ADS_PUBLISH)
.hasPermission();
}
@Override
public Ad load(final Long id, final Relationship... fetch) {
return adDao.load(id, fetch);
}
@Override
public void markMembersNotified(Ad ad) {
ad = load(ad.getId());
ad.setMembersNotified(true);
adDao.update(ad);
}
@Override
public void notifyExpiredAds(final Calendar time) {
final AdQuery searchParams = new AdQuery();
searchParams.setResultType(ResultType.ITERATOR);
searchParams.setEndDate(time);
CacheCleaner cleaner = new CacheCleaner(fetchService);
List<Ad> ads = search(searchParams);
try {
for (Ad ad : ads) {
memberNotificationHandler.expiredAdNotification(ad);
cleaner.clearCache();
}
} finally {
DataIteratorHelper.close(ads);
}
}
@Override
public void remove(final Long id) {
doRemove(id);
}
@Override
public int remove(final Long[] ids) {
return doRemove(ids);
}
@Override
public Ad save(Ad ad) {
// If the price is null, set currency to null too
if (ad.getPrice() == null) {
ad.setCurrency(null);
}
// Validates whether the Ad is valid or not
validate(ad);
// Store the custom values out of the ad
final Collection<AdCustomFieldValue> customValues = ad.getCustomValues();
ad.setCustomValues(null);
// Check for limited input
final Member owner = fetchService.fetch(ad.getOwner(), Element.Relationships.GROUP);
final MemberGroup group = owner.getMemberGroup();
final MemberGroupSettings memberSettings = group.getMemberSettings();
if (!memberSettings.isEnablePermanentAds()) {
ad.setPermanent(false);
}
final ExternalAdPublication externalAdPublication = memberSettings.getExternalAdPublication();
if (externalAdPublication == ExternalAdPublication.DISABLED) {
ad.setExternalPublication(false);
} else if (externalAdPublication == ExternalAdPublication.ENABLED) {
ad.setExternalPublication(true);
}
final boolean isInsert = ad.isTransient();
if (isInsert) {
final int maxAds = owner.getMemberGroup().getMemberSettings().getMaxAdsPerMember();
// Check if the member can publish more advertisements
final AdQuery adQuery = new AdQuery();
adQuery.setPageForCount();
adQuery.setOwner(ad.getOwner());
final int currentAds = PageHelper.getTotalCount(adDao.search(adQuery));
if (currentAds >= maxAds) {
throw new ValidationException("ad.error.maxAds", ad.getOwner().getUsername());
}
final Calendar now = Calendar.getInstance();
if (ad.isPermanent()) {
final Period p = new Period(now, null);
ad.setPublicationPeriod(p);
}
ad.setCreationDate(now);
ad = adDao.insert(ad);
} else {
final Ad old = load(ad.getId());
// Keep some properties
ad.setMembersNotified(old.isMembersNotified());
ad.setCreationDate(old.getCreationDate());
// Force the publication period for permanent ads to be the creation date. This avoids creating a
// scheduled ad in the future, then changing it to permanent, to make it appear first in the results
if (ad.isPermanent()) {
final Period p = new Period(ad.getCreationDate(), null);
ad.setPublicationPeriod(p);
}
ad = adDao.update(ad);
}
// Save the custom field values
ad.setCustomValues(customValues);
adCustomFieldService.saveValues(ad);
// Update the full text index
adDao.addToIndex(ad);
return ad;
}
@Override
public List<Ad> search(final AdQuery query) {
if (!applyLoggedUserFilters(query)) {
return Collections.emptyList();
}
setupGroupFilters(query);
return adDao.search(query);
}
public void setAdCategoryServiceLocal(final AdCategoryServiceLocal adCategoryService) {
this.adCategoryService = adCategoryService;
}
public void setAdCustomFieldServiceLocal(final AdCustomFieldServiceLocal adCustomFieldService) {
this.adCustomFieldService = adCustomFieldService;
}
public void setAdDao(final AdDAO adDao) {
this.adDao = adDao;
}
public void setAdHelper(final AdHelper adHelper) {
this.adHelper = adHelper;
}
public void setCacheManager(final CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setCustomFieldHelper(final CustomFieldHelper customFieldHelper) {
this.customFieldHelper = customFieldHelper;
}
public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
this.fetchService = fetchService;
}
public void setIndexOperationRunner(final IndexOperationRunner indexOperationRunner) {
// Whenever something in ads change, invalidate the counters cache
indexOperationRunner.addIndexOperationListener(new IndexOperationListener() {
@Override
public void onComplete(final IndexOperation operation) {
EntityType entityType = operation.getEntityType();
if (entityType == null || entityType == EntityType.ADVERTISEMENT) {
invalidateCountersCache();
}
}
});
}
public void setMemberCustomFieldServiceLocal(final MemberCustomFieldServiceLocal memberCustomFieldServiceLocal) {
this.memberCustomFieldServiceLocal = memberCustomFieldServiceLocal;
}
public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
this.memberNotificationHandler = memberNotificationHandler;
}
public void setMemberServiceLocal(final MemberServiceLocal memberServiceLocal) {
this.memberServiceLocal = memberServiceLocal;
}
public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
this.permissionService = permissionService;
}
public void setQueryHelper(final QueryHelper queryHelper) {
this.queryHelper = queryHelper;
}
public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
this.settingsService = settingsService;
}
@Override
public void validate(final Ad ad) throws ValidationException {
getValidator().validate(ad);
}
@Override
public Collection<MemberGroup> visibleGroupsForAds() {
Collection<MemberGroup> visibleGroups;
if ((LoggedUser.isMember() || LoggedUser.isOperator())) {
// Members and their operators have an specific setting of which groups they see ads
MemberGroup memberGroup = ((Member) LoggedUser.accountOwner()).getMemberGroup();
visibleGroups = fetchService.fetch(memberGroup, MemberGroup.Relationships.CAN_VIEW_ADS_OF_GROUPS).getCanViewAdsOfGroups();
} else {
visibleGroups = permissionService.getVisibleMemberGroups();
}
return visibleGroups;
}
private boolean applyLoggedUserFilters(final AbstractAdQuery query) {
if (LoggedUser.hasUser()) {
if (LoggedUser.isAdministrator()) {
// The logged user is an admin
AdminGroup adminGroup = LoggedUser.group();
adminGroup = fetchService.fetch(adminGroup, AdminGroup.Relationships.MANAGES_GROUPS);
final Collection<MemberGroup> managesGroups = adminGroup.getManagesGroups();
if (CollectionUtils.isEmpty(managesGroups)) {
return false;
}
if (CollectionUtils.isEmpty(query.getGroups())) {
query.setGroups(managesGroups);
} else {
for (final Iterator<? extends Group> iter = query.getGroups().iterator(); iter.hasNext();) {
final Group group = iter.next();
if (!managesGroups.contains(group)) {
iter.remove();
}
}
}
} else {
// If it´s a member viewing his/her own ads or it´s an operator viewing his/her member's ads
if (query.isMyAds()) {
query.setOwner((Member) LoggedUser.accountOwner());
return true;
}
// If there's a member logged on, ensure to constrain the groups he can view
MemberGroup group;
if (LoggedUser.isOperator()) {
final Operator operator = LoggedUser.element();
group = operator.getMember().getMemberGroup();
} else {
group = LoggedUser.group();
}
group = fetchService.fetch(group, MemberGroup.Relationships.CAN_VIEW_ADS_OF_GROUPS);
final Collection<MemberGroup> canViewAdsOfGroups = group.getCanViewAdsOfGroups();
if (CollectionUtils.isEmpty(canViewAdsOfGroups)) {
return false;
}
if (CollectionUtils.isEmpty(query.getGroups())) {
query.setGroups(canViewAdsOfGroups);
} else {
for (final Iterator<? extends Group> iter = query.getGroups().iterator(); iter.hasNext();) {
final Group currentGroup = iter.next();
if (!canViewAdsOfGroups.contains(currentGroup)) {
iter.remove();
}
}
}
}
}
return true;
}
private int doRemove(final Long... ids) {
// Physically remove
final int count = adDao.delete(ids);
// Update the search index
adDao.removeFromIndex(Ad.class, ids);
return count;
}
private Cache getCountersCache() {
return cacheManager.getCache("cyclos.AdCategoriesWithCounters");
}
private Validator getValidator() {
final Validator validator = new Validator("ad");
validator.general(new MaxPublicationTimeValidation());
validator.property("title").required().maxLength(100);
validator.property("description").required().add(new MaxAdDescriptionSizeValidation());
validator.property("price").positiveNonZero();
validator.property("category").required();
validator.property("tradeType").required();
validator.property("owner").required();
validator.property("publicationPeriod.begin").add(new PublicationPeriodValidation());
validator.property("publicationPeriod.end").add(new PublicationPeriodValidation());
// Custom fields
validator.chained(new DelegatingValidator(new DelegatingValidator.DelegateSource() {
@Override
public Validator getValidator() {
return adCustomFieldService.getValueValidator();
}
}));
return validator;
}
private void setupGroupFilters(final AbstractAdQuery query) {
Collection<GroupFilter> groupFilters = query.getGroupFilters();
if (CollectionUtils.isNotEmpty(groupFilters)) {
// The full text search cannot handle group filters directly. Groups must be assigned
groupFilters = fetchService.fetch(groupFilters, GroupFilter.Relationships.GROUPS);
Set<MemberGroup> groups = new HashSet<MemberGroup>();
Set<MemberGroup> xGroups = new HashSet<MemberGroup>();
if (query.getGroups() != null) {
groups.addAll(query.getGroups());
}
for (final GroupFilter groupFilter : groupFilters) {
xGroups.addAll(groupFilter.getGroups());
}
if (groups.isEmpty()) {
groups = xGroups;
} else {
groups.retainAll(xGroups);
}
query.setGroupFilters(null);
query.setGroups(groups);
}
}
}