/* * Copyright (c) 2010 Lockheed Martin Corporation * * 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. */ package org.eurekastreams.server.persistence.mappers; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eurekastreams.commons.search.ProjectionSearchRequestBuilder; import org.eurekastreams.commons.search.modelview.ModelView; import org.eurekastreams.server.domain.EntityType; import org.eurekastreams.server.persistence.mappers.cache.GetPrivateCoordinatedAndFollowedGroupIdsForUser; import org.eurekastreams.server.persistence.mappers.requests.GetEntitiesByPrefixRequest; import org.eurekastreams.server.search.modelview.DisplayEntityModelView; import org.eurekastreams.server.search.modelview.DomainGroupModelView; import org.eurekastreams.server.search.modelview.PersonModelView; import org.hibernate.search.jpa.FullTextQuery; /** * Search for stream-postable people and groups by prefix, using Hibernate Search. */ public class SearchPeopleAndGroupsByPrefix extends ReadMapper<GetEntitiesByPrefixRequest, List<DisplayEntityModelView>> { /** * Logger. */ private final Log log = LogFactory.getLog(SearchPeopleAndGroupsByPrefix.class); /** * default max results from query. */ private final Integer maxResults; /** * Search request builder. */ private final ProjectionSearchRequestBuilder searchRequestBuilder; /** * Mapper to get a list of all group ids that aren't public that a user can see activity for. */ private final GetPrivateCoordinatedAndFollowedGroupIdsForUser getGroupIdsMapper; /** * Mapper used to translate user accountId to DB id. */ private final DomainMapper<String, Long> getPersonIdByAccountIdMapper; /** * Flag to enable group searching only. */ private final boolean searchGroupsOnly; /** Regex to match characters which need to be escaped for Lucene. */ private static final String LUCENE_ESCAPE_REGEX_STRING = // \n "[\\\\\\+\\-\\!\\(\\)\\:\\^\\[\\]\\\"\\{\\}\\~\\*\\?\\|\\&]"; /** Compiled regex to match characters which need to be escaped for Lucene. */ private static final Pattern LUCENE_ESCAPE_REGEX = Pattern.compile(LUCENE_ESCAPE_REGEX_STRING); /** * Constructor. * * @param inMaxResults * the max number of results to return * @param inSearchRequestBuilder * the search request builder * @param inGetGroupIdsMapper * Mapper to get groups user has access to. * @param inGetPersonIdByAccountIdMapper * Mapper used to translate user accountId to DB id. * @param inSearchGroupsOnly * Flag to search for groups only. */ public SearchPeopleAndGroupsByPrefix(final Integer inMaxResults, final ProjectionSearchRequestBuilder inSearchRequestBuilder, final GetPrivateCoordinatedAndFollowedGroupIdsForUser inGetGroupIdsMapper, final DomainMapper<String, Long> inGetPersonIdByAccountIdMapper, final boolean inSearchGroupsOnly) { maxResults = inMaxResults; searchRequestBuilder = inSearchRequestBuilder; getGroupIdsMapper = inGetGroupIdsMapper; getPersonIdByAccountIdMapper = inGetPersonIdByAccountIdMapper; searchGroupsOnly = inSearchGroupsOnly; } /** * Search for people and groups by prefix. * * @param inRequest * The request object containing parameters for search. * @return List of DisplayEntityModelView representing people/groups matching search criteria. */ @Override public List<DisplayEntityModelView> execute(final GetEntitiesByPrefixRequest inRequest) { // build a search string that includes all of the fields for both people // and groups. // - people: firstName, lastName, preferredName // - group: name // - both: isStreamPostable, isPublic // Due to text stemming, we need to search with and without the wildcard String term = escapeSearchTerm(inRequest.getPrefix()); String searchText = String.format("+(name:(%1$s* %1$s) lastName:(%1$s* %1$s) preferredName:(%1$s* %1$s)^0.5) " + "+isStreamPostable:true %2$s", term, getGroupVisibilityClause(inRequest)); if (log.isTraceEnabled()) { log.trace("Searching for " + maxResults + " people and groups with Lucene query: " + searchText); } FullTextQuery query = searchRequestBuilder.buildQueryFromNativeSearchString(searchText); searchRequestBuilder.setPaging(query, 0, maxResults); // get the model views (via the injected cache transformer) List<ModelView> searchResults = query.getResultList(); if (log.isTraceEnabled()) { log.trace("Found " + searchResults.size() + " search results"); } // transform the list to DisplayEntityModelView List<DisplayEntityModelView> displayModelViews = new ArrayList<DisplayEntityModelView>(); for (ModelView modelView : searchResults) { DisplayEntityModelView displayModelView = new DisplayEntityModelView(); if (modelView instanceof PersonModelView && !searchGroupsOnly) { PersonModelView person = (PersonModelView) modelView; if (log.isTraceEnabled()) { log.trace("Found person '" + person.getAccountId() + " with search prefix '" + searchText + "'"); } displayModelView.setDisplayName(person.getDisplayName()); displayModelView.setStreamScopeId(person.getStreamId()); displayModelView.setType(EntityType.PERSON); displayModelView.setUniqueKey(person.getAccountId()); displayModelViews.add(displayModelView); } else if (modelView instanceof DomainGroupModelView) { DomainGroupModelView group = (DomainGroupModelView) modelView; if (log.isTraceEnabled()) { log.trace("Found domain group '" + group.getShortName() + " with search prefix '" + searchText + "'"); } displayModelView.setDisplayName(group.getName()); displayModelView.setStreamScopeId(group.getStreamId()); displayModelView.setType(EntityType.GROUP); displayModelView.setUniqueKey(group.getShortName()); displayModelViews.add(displayModelView); } } return displayModelViews; } /** * Returns search clause used to sort out groups user doesn't have access to. * * @param inRequest * The search parameters * @return Search clause used to sort out groups user doesn't have access to. */ private String getGroupVisibilityClause(final GetEntitiesByPrefixRequest inRequest) { // get user id from userKey passed from client. Long userId = getPersonIdByAccountIdMapper.execute(inRequest.getUserKey()); StringBuffer result = new StringBuffer("+(isPublic:true "); // get all the group ids followed or coordinated by current user. Set<Long> groupIds = getGroupIdsMapper.execute(userId); // If group list is greater than zero, include private group visibility clause. if (groupIds.size() != 0) { result.append("( +id:("); for (Long id : groupIds) { result.append(id + " "); } // TODO: this "-isPublic:true" is because text stemmer turns "false" into "fals" // and the query fails. Investigate Lucene API to see how to do this via object // model rather than query string generation to get around this. result.append(") -isPublic:true)"); } result.append(")"); return result.toString(); } /** * Escapes a search term for Lucene. * * @param term * Term to escape. * @return Escaped term. */ private String escapeSearchTerm(final String term) { return LUCENE_ESCAPE_REGEX.matcher(term).replaceAll("\\\\$0"); } }