/** * Copyright 2012 Apache Wave. * * 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.waveprotocol.box.server.waveserver; import com.google.common.base.Function; import com.google.common.collect.HashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.inject.Inject; import com.google.inject.name.Named; import com.google.wave.api.SearchResult; import org.waveprotocol.box.server.CoreSettings; import org.waveprotocol.box.server.util.WaveletDataUtil; import org.waveprotocol.box.server.waveserver.QueryHelper.InvalidQueryException; import org.waveprotocol.wave.model.id.IdUtil; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.wave.InvalidParticipantAddress; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.ParticipantIdUtil; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveViewData; import org.waveprotocol.wave.model.wave.data.impl.WaveViewDataImpl; import org.waveprotocol.wave.util.logging.Log; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * Search provider that reads user specific info from user data wavelet. * * @author yurize@apache.org (Yuri Zelikov) */ public class SimpleSearchProviderImpl implements SearchProvider { private static final Log LOG = Log.get(SimpleSearchProviderImpl.class); private final WaveDigester digester; private final WaveMap waveMap; private final ParticipantId sharedDomainParticipantId; private final PerUserWaveViewProvider waveViewProvider; @Inject public SimpleSearchProviderImpl(@Named(CoreSettings.WAVE_SERVER_DOMAIN) final String waveDomain, WaveDigester digester, final WaveMap waveMap, PerUserWaveViewProvider userWaveViewProvider) { this.digester = digester; this.waveMap = waveMap; this.waveViewProvider = userWaveViewProvider; sharedDomainParticipantId = ParticipantIdUtil.makeUnsafeSharedDomainParticipantId(waveDomain); } @Override public SearchResult search(final ParticipantId user, String query, int startAt, int numResults) { LOG.fine("Search query '" + query + "' from user: " + user + " [" + startAt + ", " + (startAt + numResults - 1) + "]"); Map<TokenQueryType, Set<String>> queryParams = null; try { queryParams = QueryHelper.parseQuery(query); } catch (InvalidQueryException e1) { // Invalid query param - stop and return empty search results. LOG.warning("Invalid Query. " + e1.getMessage()); return digester.generateSearchResult(user, query, null); } // Maybe should be changed in case other folders in addition to 'inbox' are // added. final boolean isAllQuery = !queryParams.containsKey(TokenQueryType.IN); final List<ParticipantId> withParticipantIds; final List<ParticipantId> creatorParticipantIds; try { String localDomain = user.getDomain(); // Build and validate. withParticipantIds = QueryHelper.buildValidatedParticipantIds(queryParams, TokenQueryType.WITH, localDomain); creatorParticipantIds = QueryHelper.buildValidatedParticipantIds(queryParams, TokenQueryType.CREATOR, localDomain); } catch (InvalidParticipantAddress e) { // Invalid address - stop and return empty search results. LOG.warning("Invalid participantId: " + e.getAddress() + " in query: " + query); return digester.generateSearchResult(user, query, null); } Multimap<WaveId, WaveletId> currentUserWavesView = createWavesViewToFilter(user, isAllQuery); Function<ReadableWaveletData, Boolean> filterWaveletsFunction = createFilterWaveletsFunction(user, isAllQuery, withParticipantIds, creatorParticipantIds); Map<WaveId, WaveViewData> results = filterWavesViewBySearchCriteria(filterWaveletsFunction, currentUserWavesView); Collection<WaveViewData> searchResult = computeSearchResult(user, startAt, numResults, queryParams, results); LOG.info("Search response to '" + query + "': " + searchResult.size() + " results, user: " + user); return digester.generateSearchResult(user, query, searchResult); } private Multimap<WaveId, WaveletId> createWavesViewToFilter(final ParticipantId user, final boolean isAllQuery) { Multimap<WaveId, WaveletId> currentUserWavesView; currentUserWavesView = HashMultimap.create(); currentUserWavesView.putAll(waveViewProvider.retrievePerUserWaveView(user)); if (isAllQuery) { // If it is the "all" query - we need to include also waves view of the // shared domain participant. currentUserWavesView.putAll(waveViewProvider.retrievePerUserWaveView(sharedDomainParticipantId)); } return currentUserWavesView; } private Function<ReadableWaveletData, Boolean> createFilterWaveletsFunction(final ParticipantId user, final boolean isAllQuery, final List<ParticipantId> withParticipantIds, final List<ParticipantId> creatorParticipantIds) { // A function to be applied by the WaveletContainer. Function<ReadableWaveletData, Boolean> matchesFunction = new Function<ReadableWaveletData, Boolean>() { @Override public Boolean apply(ReadableWaveletData wavelet) { try { return isWaveletMatchesCriteria(wavelet, user, sharedDomainParticipantId, withParticipantIds, creatorParticipantIds, isAllQuery); } catch (WaveletStateException e) { LOG.warning( "Failed to access wavelet " + WaveletName.of(wavelet.getWaveId(), wavelet.getWaveletId()), e); return false; } } }; return matchesFunction; } private Map<WaveId, WaveViewData> filterWavesViewBySearchCriteria( Function<ReadableWaveletData, Boolean> matchesFunction, Multimap<WaveId, WaveletId> currentUserWavesView) { // Must use a map with stable ordering, since indices are meaningful. Map<WaveId, WaveViewData> results = Maps.newLinkedHashMap(); // Loop over the user waves view. for (WaveId waveId : currentUserWavesView.keySet()) { Collection<WaveletId> waveletIds = currentUserWavesView.get(waveId); WaveViewData view = null; // Copy of the wave built up for search hits. for (WaveletId waveletId : waveletIds) { WaveletContainer waveletContainer = null; WaveletName waveletname = WaveletName.of(waveId, waveletId); try { waveletContainer = waveMap.getLocalWavelet(waveletname); } catch (WaveletStateException e) { LOG.severe(String.format("Failed to get local wavelet %s", waveletname.toString()), e); } // TODO (Yuri Z.) This loop collects all the wavelets that match the // query, so the view is determined by the query. Instead we should // look at the user's wave view and determine if the view matches the query. try { if (waveletContainer == null || !waveletContainer.applyFunction(matchesFunction)) { continue; } if (view == null) { view = WaveViewDataImpl.create(waveId); } // Just keep adding all the relevant wavelets in this wave. view.addWavelet(waveletContainer.copyWaveletData()); } catch (WaveletStateException e) { LOG.warning("Failed to access wavelet " + waveletContainer.getWaveletName(), e); } } if (view != null) { results.put(waveId, view); } } return results; } /** * Verifies whether the wavelet matches the filter criteria. * * @param wavelet the wavelet. * @param user the logged in user. * @param sharedDomainParticipantId the shared domain participant id. * @param withList the list of participants to be used in 'with' filter. * @param creatorList the list of participants to be used in 'creator' filter. * @param isAllQuery true if the search results should include shared for this * domain waves. */ private boolean isWaveletMatchesCriteria(ReadableWaveletData wavelet, ParticipantId user, ParticipantId sharedDomainParticipantId, List<ParticipantId> withList, List<ParticipantId> creatorList, boolean isAllQuery) throws WaveletStateException { // If it is user data wavelet for the user - return true. if (IdUtil.isUserDataWavelet(wavelet.getWaveletId()) && wavelet.getCreator().equals(user)) { return true; } // Filter by creator. This is the fastest check so we perform it first. for (ParticipantId creator : creatorList) { if (!creator.equals(wavelet.getCreator())) { // Skip. return false; } } // The wavelet should have logged in user as participant for 'in:inbox' // query. if (!isAllQuery && !wavelet.getParticipants().contains(user)) { return false; } // Or if it is an 'all' query - then either logged in user or shared domain // participant should be present in the wave. if (isAllQuery && !WaveletDataUtil.checkAccessPermission(wavelet, user, sharedDomainParticipantId)) { return false; } // If not returned 'false' above - then logged in user is either // explicit or implicit participant and therefore has access permission. // Now filter by 'with'. for (ParticipantId otherUser : withList) { if (!wavelet.getParticipants().contains(otherUser)) { // Skip. return false; } } return true; } private Collection<WaveViewData> computeSearchResult(final ParticipantId user, int startAt, int numResults, Map<TokenQueryType, Set<String>> queryParams, Map<WaveId, WaveViewData> results) { List<WaveViewData> searchResultslist = null; int searchResultSize = results.values().size(); // Check if we have enough results to return. if (searchResultSize < startAt) { searchResultslist = Collections.emptyList(); } else { int endAt = Math.min(startAt + numResults, searchResultSize); searchResultslist = QueryHelper.computeSorter(queryParams).sortedCopy(results.values()) .subList(startAt, endAt); } return searchResultslist; } }