/* 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.dao.ads; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import nl.strohalm.cyclos.dao.IndexedDAOImpl; import nl.strohalm.cyclos.entities.ads.Ad; import nl.strohalm.cyclos.entities.ads.Ad.Status; 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.AdCustomFieldValue; import nl.strohalm.cyclos.entities.customization.fields.MemberCustomFieldValue; import nl.strohalm.cyclos.entities.customization.images.AdImage; import nl.strohalm.cyclos.entities.exceptions.DaoException; import nl.strohalm.cyclos.entities.exceptions.QueryParseException; import nl.strohalm.cyclos.entities.groups.Group; import nl.strohalm.cyclos.entities.groups.MemberGroup; import nl.strohalm.cyclos.utils.DateHelper; import nl.strohalm.cyclos.utils.EntityHelper; import nl.strohalm.cyclos.utils.Period; import nl.strohalm.cyclos.utils.TimePeriod; import nl.strohalm.cyclos.utils.conversion.CoercionHelper; import nl.strohalm.cyclos.utils.hibernate.HibernateCustomFieldHandler; import nl.strohalm.cyclos.utils.hibernate.HibernateHelper; import nl.strohalm.cyclos.utils.lucene.Filters; import nl.strohalm.cyclos.utils.lucene.LuceneUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryParser.MultiFieldQueryParser; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.search.Filter; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.apache.lucene.search.TotalHitCountCollector; /** * Implementation class for ad DAO * @author rafael */ public class AdDAOImpl extends IndexedDAOImpl<Ad> implements AdDAO { private static class FullTextSearchElements { private final Query query; private final Filters filters; private final Sort sort; public FullTextSearchElements(final Query query, final Filters filters, final Sort sort) { this.query = query; this.filters = filters; this.sort = sort; } public Filters getFilters() { return filters; } public Query getQuery() { return query; } public Sort getSort() { return sort; } } private static final String[] FIELDS_FULL_TEXT = { "title", "description", "customValues", "owner.name", "owner.email", "owner.username", "owner.customValues" }; private HibernateCustomFieldHandler hibernateCustomFieldHandler; public AdDAOImpl() { super(Ad.class); } @Override public int delete(final boolean flush, final Long... ids) { if (ids != null && ids.length > 0) { final Map<String, Object> namedParameters = new HashMap<String, Object>(); namedParameters.put("ids", Arrays.asList(ids)); bulkUpdate("delete from " + AdCustomFieldValue.class.getName() + " v where v.ad.id in (:ids)", namedParameters); bulkUpdate("delete from " + AdImage.class.getName() + " ai where ai.ad.id in (:ids)", namedParameters); final Integer results = CoercionHelper.coerce(Integer.TYPE, bulkUpdate("update Ad ad set ad.deleteDate = current_date(), ad.description = null where ad.id in (:ids)", namedParameters)); if (flush) { flush(); } return results; } else { return 0; } } @Override public List<Ad> fullTextSearch(final FullTextAdQuery adQuery) throws DaoException { FullTextSearchElements elements = prepare(adQuery); return list(Ad.class, adQuery, elements.getQuery(), elements.getFilters(), elements.getSort()); } @Override public List<AdCategoryWithCounterVO> getCategoriesWithCounters(final List<AdCategory> categories, final AdCategoryWithCounterQuery acQuery) { IndexReader reader = null; IndexSearcher searcher = null; List<AdCategoryWithCounterVO> result = new ArrayList<AdCategoryWithCounterVO>(categories.size()); try { reader = indexHandler.openReader(Ad.class); searcher = new IndexSearcher(reader); FullTextAdQuery adQuery = new FullTextAdQuery(); adQuery.setStatus(Ad.Status.ACTIVE); adQuery.setTradeType(acQuery.getTradeType()); if (acQuery.isExternalPublication()) { adQuery.setExternalPublication(true); } Long[] groupIds = acQuery.getGroupIds(); if (groupIds != null) { adQuery.setGroups(Arrays.asList(EntityHelper.references(MemberGroup.class, Arrays.asList(groupIds)))); } FullTextSearchElements elements = prepare(adQuery); Query query = elements.getQuery(); Filters baseFilters = elements.getFilters(); for (AdCategory adCategory : categories) { AdCategoryWithCounterVO counter = createCounter(searcher, query, baseFilters, adCategory, null, 1); result.add(counter); } return result; } catch (Exception e) { throw new DaoException(e); } finally { try { searcher.close(); } catch (final Exception e) { // Silently ignore } try { reader.close(); } catch (final Exception e) { // Silently ignore } } } @Override public Integer getNumberOfAds(final Calendar date, final Collection<? extends Group> groups, final Ad.Status status) { return count(date, groups, status, "count(ad.id)"); } @Override public Integer getNumberOfCreatedAds(final Period period, final Collection<? extends Group> groups) { final Map<String, Object> namedParameters = new HashMap<String, Object>(); final StringBuilder hql = new StringBuilder("select count(ad.id) from Ad ad where 1=1"); HibernateHelper.addPeriodParameterToQuery(hql, namedParameters, "ad.creationDate", period); if (!CollectionUtils.isEmpty(groups)) { hql.append(" and ad.owner.group in (:groups) "); namedParameters.put("groups", groups); } return uniqueResult(hql.toString(), namedParameters); } @Override public Integer getNumberOfMembersWithAds(final Calendar date, final Collection<? extends Group> groups) throws DaoException { return count(date, groups, Ad.Status.ACTIVE, "count(distinct ad.owner.id)"); } @Override public List<Ad> search(final AdQuery query) { final Map<String, Object> namedParameters = new HashMap<String, Object>(); final StringBuilder hql = new StringBuilder(); hql.append(" select ad"); hql.append(" from Ad ad inner join ad.owner m left join ad.category c1 left join c1.parent c2 left join c2.parent c3 "); hibernateCustomFieldHandler.appendJoins(hql, "ad.customValues", query.getAdValues()); hibernateCustomFieldHandler.appendJoins(hql, "m.customValues", query.getMemberValues()); HibernateHelper.appendJoinFetch(hql, getEntityType(), "ad", query.getFetch()); hql.append(" where 1=1"); if (query.getCategory() != null) { hql.append(" and (c1 = :adCategory or c2 = :adCategory or c3 = :adCategory)"); namedParameters.put("adCategory", query.getCategory()); } if (!query.isIncludeDeleted()) { hql.append(" and ad.deleteDate is null "); } HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.category.active", true); HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.id", query.getId()); HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.membersNotified", query.getMembersNotified()); HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.externalPublication", query.getExternalPublication()); HibernateHelper.addParameterToQuery(hql, namedParameters, "m", query.getOwner()); // Group filters are handled at service level HibernateHelper.addInParameterToQuery(hql, namedParameters, "m.group", query.getGroups()); HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.tradeType", query.getTradeType()); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.price", ">=", query.getInitialPrice()); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.price", "<=", query.getFinalPrice()); HibernateHelper.addParameterToQuery(hql, namedParameters, "ad.currency", query.getCurrency()); HibernateHelper.addPeriodParameterToQuery(hql, namedParameters, "ad.publicationPeriod.begin", Period.day(query.getBeginDate())); HibernateHelper.addPeriodParameterToQuery(hql, namedParameters, "ad.publicationPeriod.end", Period.day(query.getEndDate())); final Calendar now = Calendar.getInstance(); // Since if (query.getSince() != null && query.getSince().getNumber() > 0) { final Calendar since = DateHelper.truncate(query.getSince().remove(now)); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.publicationPeriod.begin", ">=", since); } HibernateHelper.addPeriodParameterToQuery(hql, namedParameters, "ad.publicationPeriod.begin", query.getPeriod()); // With Images Only if (query.isWithImagesOnly()) { hql.append(" and exists (select img.id from AdImage img where img.ad = ad) "); } // Check the history date final Calendar historyDate = (Calendar) ObjectUtils.defaultIfNull(query.getHistoryDate(), Calendar.getInstance()); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.creationDate", "<=", historyDate); hql.append(" and (ad.deleteDate is null or ad.deleteDate >= :historyDate)"); namedParameters.put("historyDate", historyDate); // Status if (query.getStatus() != null) { switch (query.getStatus()) { case PERMANENT: hql.append(" and ad.permanent = true "); break; case ACTIVE: hql.append(" and (ad.permanent = true or ((ad.publicationPeriod.end is null or ad.publicationPeriod.end >= :historyDate) and ad.publicationPeriod.begin <= :historyDate)) "); break; case SCHEDULED: hql.append(" and (ad.permanent is null or ad.permanent = false) and ad.publicationPeriod.begin > :historyDate "); break; case EXPIRED: hql.append(" and (ad.permanent is null or ad.permanent = false) and ad.publicationPeriod.end < :historyDate "); break; } } // Keywords if (StringUtils.isNotEmpty(query.getKeywords())) { hql.append(" and ((ad.title like :keywords) or (ad.description like :keywords))"); namedParameters.put("keywords", "%" + query.getKeywords() + "%"); } // Custom Values hibernateCustomFieldHandler.appendConditions(hql, namedParameters, query.getAdValues()); hibernateCustomFieldHandler.appendConditions(hql, namedParameters, query.getMemberValues()); // Handle order if (query.isRandomOrder()) { HibernateHelper.appendOrder(hql, "rand()"); } else { HibernateHelper.appendOrder(hql, "ad.publicationPeriod.begin desc, ad.id desc"); } return list(query, hql.toString(), namedParameters); } public void setHibernateCustomFieldHandler(final HibernateCustomFieldHandler hibernateCustomFieldHandler) { this.hibernateCustomFieldHandler = hibernateCustomFieldHandler; } private Integer count(Calendar date, final Collection<? extends Group> groups, final Ad.Status status, final String projection) { if (date == null) { date = Calendar.getInstance(); } final Map<String, Object> namedParameters = new HashMap<String, Object>(); final StringBuilder hql = new StringBuilder("select " + projection + " from Ad ad where 1=1"); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.creationDate", "<=", date); hql.append(" and (ad.deleteDate is null or ad.deleteDate > :date)"); if (!CollectionUtils.isEmpty(groups)) { hql.append(" and ad.owner.group in (:groups) "); namedParameters.put("groups", groups); } switch (status) { case ACTIVE: hql.append(" and ad.publicationPeriod.begin <= :date"); hql.append(" and (ad.permanent = true or ad.publicationPeriod.end > :date)"); break; case PERMANENT: hql.append(" and ad.publicationPeriod.begin <= :date"); hql.append(" and ad.permanent = true"); break; case EXPIRED: HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.publicationPeriod.begin", "<=", date); HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.publicationPeriod.end", "<=", date); break; case SCHEDULED: HibernateHelper.addParameterToQueryOperator(hql, namedParameters, "ad.publicationPeriod.begin", ">", date); break; } namedParameters.put("date", date); return uniqueResult(hql.toString(), namedParameters); } private AdCategoryWithCounterVO createCounter(final IndexSearcher searcher, final Query query, final Filters baseFilters, final AdCategory adCategory, final AdCategoryWithCounterVO parent, final int level) throws IOException { // Run with filters based on the current category Filters filters = (Filters) baseFilters.clone(); filters.addTerms("category", adCategory.getId()); TotalHitCountCollector collector = new TotalHitCountCollector(); searcher.search(query, filters, collector); int totalCount = collector.getTotalHits(); AdCategoryWithCounterVO counter = new AdCategoryWithCounterVO(adCategory.getId(), adCategory.getName(), level, totalCount, parent); // Repeat recursively for each child for (AdCategory childCategory : adCategory.getChildren()) { AdCategoryWithCounterVO childCounter = createCounter(searcher, query, baseFilters, childCategory, counter, level + 1); counter.addChild(childCounter); } return counter; } private MultiFieldQueryParser getQueryParser(final Analyzer analyzer) { final Map<String, Float> boosts = new HashMap<String, Float>(); boosts.put("title", 2.5F); boosts.put("description", 2F); boosts.put("owner.name", 1.5F); boosts.put("owner.username", 1.3F); return new MultiFieldQueryParser(LuceneUtils.LUCENE_VERSION, FIELDS_FULL_TEXT, analyzer, boosts); } private FullTextSearchElements prepare(final FullTextAdQuery adQuery) { final String keywords = adQuery.getKeywords(); final Calendar today = DateHelper.truncate(Calendar.getInstance()); Sort sort = null; Query query; Analyzer analyzer = adQuery.getAnalyzer(); if (keywords == null) { query = new MatchAllDocsQuery(); // When not using keywords, return newer first sort = new Sort(new SortField("baseDate", SortField.STRING, true)); } else { try { query = getQueryParser(analyzer).parse(keywords); } catch (final ParseException e) { throw new QueryParseException(e); } } final Filters filters = new Filters(); filters.addTerms("id", adQuery.getId()); filters.addTerms("membersNotified", adQuery.getMembersNotified()); filters.addTerms("category", adQuery.getCategoriesIds()); filters.addTerms("currency", adQuery.getCurrency()); filters.addTerms("externalPublication", adQuery.getExternalPublication()); filters.addRange("price", adQuery.getInitialPrice(), adQuery.getFinalPrice()); final TimePeriod since = adQuery.getSince(); if (since != null && since.isValid()) { final Calendar sinceDate = since.remove(today); filters.addRange("publicationBegin", sinceDate, null); } filters.addPeriod("publicationBegin", adQuery.getPeriod()); filters.addTerms("owner", adQuery.getOwner()); filters.addTerms("owner.group", adQuery.getGroups()); filters.addTerms("tradeType", adQuery.getTradeType()); if (CollectionUtils.isNotEmpty(adQuery.getAdValues())) { for (final AdCustomFieldValue fieldValue : adQuery.getAdValues()) { addCustomField(filters, analyzer, fieldValue); } } if (CollectionUtils.isNotEmpty(adQuery.getMemberValues())) { for (final MemberCustomFieldValue fieldValue : adQuery.getMemberValues()) { addCustomField(filters, analyzer, fieldValue, "owner.customValues.%s"); } } if (adQuery.isWithImagesOnly()) { filters.addTerms("hasImages", true); } // Status final Status status = adQuery.getStatus(); if (status != null) { final Filter isPermanent = Filters.terms("permanent", true); final Filter isNotPermanent = Filters.terms("permanent", false); Filter endRange; Filter beginRange; switch (status) { case PERMANENT: // permanent = true filters.add(isPermanent); break; case ACTIVE: // permanent = true or (end > today and begin <= today) // neither begin / end are null beginRange = Filters.range("publicationBegin", null, today); endRange = Filters.range("publicationEnd", today, null); filters.add(Filters.or(isPermanent, Filters.and(endRange, beginRange))); break; case SCHEDULED: // permanent = false and begin >= today beginRange = Filters.range("publicationBegin", today, null); filters.add(Filters.and(isNotPermanent, beginRange)); break; case EXPIRED: // permanent = false and end <= today endRange = Filters.range("publicationEnd", null, today, false, false); filters.add(Filters.and(isNotPermanent, endRange)); break; } } return new FullTextSearchElements(query, filters, sort); } }