/* * * This is a simple Content Management System (CMS) * Copyright (C) 2009 Imran M Yousuf (imyousuf@smartitengineering.com) * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.smartitengineering.cms.spi.impl.content.search; import com.google.inject.Inject; import com.google.inject.name.Named; import com.smartitengineering.cms.api.content.Content; import com.smartitengineering.cms.api.content.ContentId; import com.smartitengineering.cms.api.content.Filter; import com.smartitengineering.cms.api.common.SearchResult; import com.smartitengineering.cms.api.event.Event.EventType; import com.smartitengineering.cms.api.event.Event.Type; import com.smartitengineering.cms.api.event.EventListener; import com.smartitengineering.cms.api.factory.SmartContentAPI; import com.smartitengineering.cms.api.type.ContentStatus; import com.smartitengineering.cms.api.type.ContentTypeId; import com.smartitengineering.cms.api.workspace.WorkspaceId; import com.smartitengineering.cms.spi.content.ContentSearcher; import com.smartitengineering.cms.spi.impl.SearchBeanLoader; import com.smartitengineering.cms.spi.impl.events.SolrFieldNames; import com.smartitengineering.common.dao.search.CommonFreeTextSearchDao; import com.smartitengineering.dao.common.queryparam.BasicCompoundQueryParameter; import com.smartitengineering.dao.common.queryparam.BiOperandQueryParameter; import com.smartitengineering.dao.common.queryparam.MatchMode; import com.smartitengineering.dao.common.queryparam.OperatorType; import com.smartitengineering.dao.common.queryparam.Order; import com.smartitengineering.dao.common.queryparam.ParameterType; import com.smartitengineering.dao.common.queryparam.QueryParameter; import com.smartitengineering.dao.common.queryparam.QueryParameterCastHelper; import com.smartitengineering.dao.common.queryparam.QueryParameterFactory; import com.smartitengineering.dao.common.queryparam.StringLikeQueryParameter; import com.smartitengineering.dao.common.queryparam.UniOperandQueryParameter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateFormatUtils; import org.apache.solr.client.solrj.util.ClientUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author kaisar */ public class ContentSearcherImpl implements ContentSearcher { public static final String REINDEX_LISTENER_NAME = "contentReindexEventListener"; protected final transient Logger logger = LoggerFactory.getLogger(getClass()); @Inject private CommonFreeTextSearchDao<Content> textSearchDao; @Inject private SearchBeanLoader<Content, ContentId> contentLoader; // Injected so that the quartz service starts @Inject @Named(REINDEX_LISTENER_NAME) private EventListener reindexListener; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final String disjunctionSeperator = " OR "; private final String conjunctionSeperator = " AND "; private static final String SOLR_DATE_FORMAT = DateFormatUtils.ISO_DATETIME_FORMAT.getPattern() + "'Z'"; @Override public SearchResult<Content> search(Filter filter) { final StringBuilder finalQuery = new StringBuilder(); String seperator = filter.isDisjunction() ? disjunctionSeperator : conjunctionSeperator; int count = 0; Set<ContentTypeId> contentTypeIds = filter.getContentTypeFilters(); finalQuery.append(SolrFieldNames.TYPE).append(": ").append(ContentHelper.CONTENT); final WorkspaceId workspaceId = filter.getWorkspaceId(); if (workspaceId != null) { finalQuery.append(conjunctionSeperator); finalQuery.append((" (")); finalQuery.append(SolrFieldNames.WORKSPACEID).append(": ").append(ClientUtils.escapeQueryChars( workspaceId.toString())); if (filter.isFriendliesIncluded()) { Collection<WorkspaceId> friendlies = workspaceId.getWorkspace().getFriendlies(); if (friendlies != null && !friendlies.isEmpty()) { finalQuery.append(disjunctionSeperator).append("(private: false AND ("); boolean first = true; for (WorkspaceId friendly : friendlies) { if (friendly != null) { if (first) { first = false; } else { finalQuery.append(disjunctionSeperator); } finalQuery.append(SolrFieldNames.WORKSPACEID).append(": ").append(ClientUtils.escapeQueryChars(friendly. toString())); } } finalQuery.append("))"); } } finalQuery.append((") ")); } final StringBuilder query = new StringBuilder(); if (contentTypeIds != null && !contentTypeIds.isEmpty()) { if (query.length() > 0) { query.append(seperator); } query.append("("); } for (ContentTypeId contentTypeId : contentTypeIds) { if (count > 0) { query.append(disjunctionSeperator); } if (contentTypeId != null) { query.append(SolrFieldNames.INSTANCE_OF).append(": ").append(ClientUtils.escapeQueryChars( contentTypeId.toString())); } count++; } if (contentTypeIds != null && !contentTypeIds.isEmpty()) { query.append(")"); } if (StringUtils.isNotBlank(filter.getSearchTerms())) { if (query.length() > 0) { query.append(seperator); } query.append(SolrFieldNames.ALL_TEXT).append(": ").append(ClientUtils.escapeQueryChars(filter.getSearchTerms())); } Set<ContentStatus> statuses = filter.getStatusFilters(); for (ContentStatus contentStatus : statuses) { if (query.length() > 0) { query.append(seperator); } if (StringUtils.isNotBlank(contentStatus.getName())) { query.append(SolrFieldNames.STATUS).append(": ").append(ClientUtils.escapeQueryChars(contentStatus.getName())); } } Collection<QueryParameter> fieldQuery = new ArrayList<QueryParameter>(filter.getFieldFilters()); final QueryParameter orderParam = findAndRemoveOrderByParam(fieldQuery); processParams(fieldQuery, query, seperator, filter.isFieldParamsEscaped()); if (filter.getCreationDateFilter() != null) { if (query.length() > 0) { query.append(seperator); } else { query.append("contentTypeId: [* TO *]").append(seperator); } QueryParameter<Date> creationDateFilter = filter.getCreationDateFilter(); String queryStr = generateDateQuery(SolrFieldNames.CREATIONDATE, creationDateFilter); query.append(queryStr); } if (filter.getLastModifiedDateFilter() != null) { if (query.length() > 0) { query.append(seperator); } else { query.append("contentTypeId: [* TO *]").append(seperator); } QueryParameter<Date> lastModifiedDateFilter = filter.getLastModifiedDateFilter(); String queryStr = generateDateQuery(SolrFieldNames.LASTMODIFIEDDATE, lastModifiedDateFilter); query.append(queryStr); } if (query.length() > 0) { finalQuery.append(conjunctionSeperator).append('(').append(query.toString()).append(')'); } if (logger.isDebugEnabled()) { logger.debug("Query q = " + finalQuery.toString()); } final QueryParameter sortParam; if (orderParam != null) { sortParam = orderParam; } else { sortParam = QueryParameterFactory.getOrderByParam(SolrFieldNames.CREATIONDATE, Order.DESC); } final com.smartitengineering.common.dao.search.SearchResult<Content> searchResult; searchResult = textSearchDao.detailedSearch(QueryParameterFactory.getStringLikePropertyParam("q", finalQuery. toString()), QueryParameterFactory.getFirstResultParam(filter.getStartFrom()), QueryParameterFactory. getMaxResultsParam(filter.getMaxContents()), sortParam); final Collection<Content> result; if (searchResult == null || searchResult.getResult() == null || searchResult.getResult().isEmpty()) { result = Collections.emptyList(); } else { result = new ArrayList<Content>(); for (Content content : searchResult.getResult()) { if (content != null) { result.add(content); } } } return SmartContentAPI.getInstance().getContentLoader().createSearchResult(result, searchResult.getTotalResults()); } protected void processParams(Collection<QueryParameter> queries, final StringBuilder query, String seperator, boolean paramsEscaped) throws IllegalArgumentException { if (queries != null && !queries.isEmpty()) { for (QueryParameter parameter : queries) { if (parameter.getParameterType().equals(ParameterType.PARAMETER_TYPE_PROPERTY) && parameter instanceof StringLikeQueryParameter) { if (query.length() > 0) { query.append(seperator); } StringLikeQueryParameter param = QueryParameterCastHelper.STRING_PARAM_HELPER.cast(parameter); query.append(param.getPropertyName()).append(": ").append(paramsEscaped ? param.getValue() : ClientUtils.escapeQueryChars(param.getValue())); } else if (parameter.getParameterType().equals(ParameterType.PARAMETER_TYPE_CONJUNCTION)) { BasicCompoundQueryParameter compoundQueryParameter = (BasicCompoundQueryParameter) parameter; StringBuilder nestedJunction = new StringBuilder(); processParams(compoundQueryParameter.getNestedParameters(), nestedJunction, conjunctionSeperator, paramsEscaped); if (nestedJunction.length() > 0) { if (query.length() > 0) { query.append(seperator); } query.append('(').append(nestedJunction).append(')'); } } else if (parameter.getParameterType().equals(ParameterType.PARAMETER_TYPE_DISJUNCTION)) { BasicCompoundQueryParameter compoundQueryParameter = (BasicCompoundQueryParameter) parameter; StringBuilder nestedJunction = new StringBuilder(); processParams(compoundQueryParameter.getNestedParameters(), nestedJunction, disjunctionSeperator, paramsEscaped); if (nestedJunction.length() > 0) { if (query.length() > 0) { query.append(seperator); } query.append('(').append(nestedJunction).append(')'); } } } } } public static String generateDateQuery(String fieldName, QueryParameter<Date> creationDateFilter) { StringBuilder query = new StringBuilder(fieldName).append(": "); String dateQuery = ""; switch (creationDateFilter.getParameterType()) { case PARAMETER_TYPE_PROPERTY: if (creationDateFilter instanceof UniOperandQueryParameter) { UniOperandQueryParameter<Date> param = (UniOperandQueryParameter<Date>) creationDateFilter; switch (param.getOperatorType()) { case OPERATOR_EQUAL: dateQuery = formatDateInSolrFormat(param.getValue()); break; case OPERATOR_LESSER: query.insert(0, "NOT "); dateQuery = "[" + formatDateInSolrFormat(param.getValue()) + " TO *]"; // dateQuery = "-[" + param.getValue() + " TO *]"; break; case OPERATOR_GREATER_EQUAL: dateQuery = "[" + formatDateInSolrFormat(param.getValue()) + " TO *]"; break; case OPERATOR_GREATER: query.insert(0, "NOT "); dateQuery = "[* TO " + formatDateInSolrFormat(param.getValue()) + "]"; // dateQuery = "-[* TO " + param.getValue() + "]"; break; case OPERATOR_LESSER_EQUAL: dateQuery = "[* TO " + formatDateInSolrFormat(param.getValue()) + "]"; break; default: dateQuery = "[* TO *]"; } } if (creationDateFilter instanceof BiOperandQueryParameter) { BiOperandQueryParameter<Date> param = (BiOperandQueryParameter<Date>) creationDateFilter; if (param.getOperatorType().equals(OperatorType.OPERATOR_BETWEEN)) { dateQuery = "[" + formatDateInSolrFormat(param.getFirstValue()) + " TO " + formatDateInSolrFormat(param. getSecondValue()) + "]"; } } break; default: UniOperandQueryParameter<Date> param = (UniOperandQueryParameter<Date>) creationDateFilter; dateQuery = param.getPropertyName() + ": [* TO *]"; break; } query.append(dateQuery); return query.toString(); } public static String formatDateInSolrFormat(Date date) { return DateFormatUtils.formatUTC(date, SOLR_DATE_FORMAT); } @Override public void reIndex(ContentId contentId) { if (contentId != null) { Content content = contentLoader.loadById(contentId); if (content != null) { reindexListener.notify(SmartContentAPI.getInstance().getEventRegistrar().<Content>createEvent( EventType.CREATE, Type.CONTENT, content)); } } } @Override public void reIndex(final WorkspaceId workspaceId) { if (logger.isInfoEnabled()) { logger.info(new StringBuilder("Re-Indexing ").append(workspaceId).toString()); } executorService.submit(new Runnable() { @Override public void run() { try { final QueryParameter param; if (workspaceId == null) { param = null; } else { param = QueryParameterFactory.getStringLikePropertyParam("id", new StringBuilder(workspaceId.toString()). append(':').toString(), MatchMode.START); } final QueryParameter<Integer> maxResultsParam = QueryParameterFactory.getMaxResultsParam(100); boolean hasMore = true; ContentId lastId = null; List<QueryParameter> params = new ArrayList<QueryParameter>(); logger.debug("Beginning iteration over contents"); while (hasMore) { if (logger.isDebugEnabled()) { logger.debug("Trying with Last ID " + lastId); } params.clear(); if (param != null) { params.add(param); } params.add(maxResultsParam); if (lastId != null) { try { params.add(QueryParameterFactory.getGreaterThanPropertyParam("id", contentLoader.getByteArrayFromId( lastId))); } catch (Exception ex) { logger.warn("Could not add last id clause " + lastId.toString(), ex); } } List<Content> list = contentLoader.getQueryResult(params); if (logger.isDebugEnabled()) { logger.debug("Has More " + hasMore); logger.debug("Content numbers in current iteration " + (list != null ? list.size() : -1)); } if (list == null || list.isEmpty()) { hasMore = false; } else { final Content[] contents = new Content[list.size()]; int index = 0; for (Content content : list) { if (logger.isDebugEnabled()) { logger.debug("Attempting to index " + content.getContentId()); } reindexListener.notify(SmartContentAPI.getInstance().getEventRegistrar().<Content>createEvent( EventType.UPDATE, Type.CONTENT, content)); contents[index++] = content; } lastId = contents[contents.length - 1].getContentId(); } if (logger.isDebugEnabled()) { logger.debug("Has More " + hasMore); logger.debug("Content numbers in current iteration " + (list != null ? list.size() : -1)); logger.debug("Last ID " + lastId); logger.debug("Going for next iteration " + hasMore); } } } catch (Exception ex) { logger.error("Error reindexing", ex); } } }); } private QueryParameter findAndRemoveOrderByParam(Collection<QueryParameter> fieldQuery) { Iterator<QueryParameter> iterator = fieldQuery.iterator(); while (iterator.hasNext()) { QueryParameter parameter = iterator.next(); if (parameter.getParameterType().equals(ParameterType.PARAMETER_TYPE_ORDER_BY)) { iterator.remove(); return parameter; } } return null; } @Override public String escapeStringForSearch(String string) { return ClientUtils.escapeQueryChars(string); } }