nl.strohalm.cyclos.services.ads.AdServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.ads.AdServiceImpl.java

Source

/*
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 its a member viewing his/her own ads or its 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);
        }
    }
}