/** * Copyright 2010 Google Inc. * * 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.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import com.google.inject.Inject; import com.google.inject.name.Named; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.box.common.ExceptionalIterator; import org.waveprotocol.box.server.CoreSettings; import org.waveprotocol.box.server.persistence.PersistenceException; import org.waveprotocol.box.server.util.WaveletDataUtil; import org.waveprotocol.box.server.waveserver.WaveBus.Subscriber; 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.operation.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.version.HashedVersion; 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.ObservableWaveletData; 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.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * A collection of wavelets, local and remote, held in memory. * * @author soren@google.com (Soren Lassen) */ public class WaveMap implements SearchProvider { /** * Helper class that allows to add basic sort and filter functionality to the * search. * * @author vega113@gmail.com (Yuri Z.) */ private static class QueryHelper { @SuppressWarnings("serial") class InvalidQueryException extends Exception { public InvalidQueryException(String msg) { super(msg); } } /** * Unknown participantId used by {@link ASC_CREATOR_COMPARATOR} in case wave * creator cannot be found. */ static final ParticipantId UNKNOWN_CREATOR = ParticipantId.ofUnsafe("unknown@example.com"); /** Sorts search result in ascending order by LMT. */ static final Comparator<WaveViewData> ASC_LMT_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { long lmt0 = computeLmt(arg0); long lmt1 = computeLmt(arg1); return Long.signum(lmt0 - lmt1); } private long computeLmt(WaveViewData wave) { long lmt = -1; for (ObservableWaveletData wavelet : wave.getWavelets()) { // Skip non conversational wavelets. if (!IdUtil.isConversationalId(wavelet.getWaveletId())) { continue; } lmt = lmt < wavelet.getLastModifiedTime() ? wavelet.getLastModifiedTime() : lmt; } return lmt; } }; /** Sorts search result in descending order by LMT. */ static final Comparator<WaveViewData> DESC_LMT_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { return -ASC_LMT_COMPARATOR.compare(arg0, arg1); } }; /** Sorts search result in ascending order by creation time. */ static final Comparator<WaveViewData> ASC_CREATED_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { long time0 = computeCreatedTime(arg0); long time1 = computeCreatedTime(arg1); return Long.signum(time0 - time1); } private long computeCreatedTime(WaveViewData wave) { long creationTime = -1; for (ObservableWaveletData wavelet : wave.getWavelets()) { creationTime = creationTime < wavelet.getCreationTime() ? wavelet.getCreationTime() : creationTime; } return creationTime; } }; /** Sorts search result in descending order by creation time. */ static final Comparator<WaveViewData> DESC_CREATED_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { return -ASC_CREATED_COMPARATOR.compare(arg0, arg1); } }; /** Sorts search result in ascending order by creator */ static final Comparator<WaveViewData> ASC_CREATOR_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { ParticipantId creator0 = computeCreator(arg0); ParticipantId creator1 = computeCreator(arg1); return creator0.compareTo(creator1); } private ParticipantId computeCreator(WaveViewData wave) { for (ObservableWaveletData wavelet : wave.getWavelets()) { if (IdUtil.isConversationRootWaveletId(wavelet.getWaveletId())) { return wavelet.getCreator(); } } // If not found creator - compare with UNKNOWN_CREATOR; return UNKNOWN_CREATOR; } }; /** Sorts search result in descending order by creator */ static final Comparator<WaveViewData> DESC_CREATOR_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { return -ASC_CREATOR_COMPARATOR.compare(arg0, arg1); } }; /** Sorts search result by WaveId. */ static final Comparator<WaveViewData> ID_COMPARATOR = new Comparator<WaveViewData>() { @Override public int compare(WaveViewData arg0, WaveViewData arg1) { return arg0.getWaveId().compareTo(arg1.getWaveId()); } }; /** * Orders using {@link ASCENDING_DATE_COMPARATOR}. */ static final Ordering<WaveViewData> ASC_LMT_ORDERING = Ordering .from(QueryHelper.ASC_LMT_COMPARATOR); /** * Orders using {@link DESCENDING_DATE_COMPARATOR}. */ static final Ordering<WaveViewData> DESC_LMT_ORDERING = Ordering .from(QueryHelper.DESC_LMT_COMPARATOR); /** * Orders using {@link ASC_CREATED_COMPARATOR}. */ static final Ordering<WaveViewData> ASC_CREATED_ORDERING = Ordering .from(QueryHelper.ASC_CREATED_COMPARATOR); /** * Orders using {@link DESC_CREATED_COMPARATOR}. */ static final Ordering<WaveViewData> DESC_CREATED_ORDERING = Ordering .from(QueryHelper.DESC_CREATED_COMPARATOR); /** * Orders using {@link ASC_CREATOR_COMPARATOR}. */ static final Ordering<WaveViewData> ASC_CREATOR_ORDERING = Ordering .from(QueryHelper.ASC_CREATOR_COMPARATOR); /** * Orders using {@link DESC_CREATOR_COMPARATOR}. */ static final Ordering<WaveViewData> DESC_CREATOR_ORDERING = Ordering .from(QueryHelper.DESC_CREATOR_COMPARATOR); /** Default ordering is by LMT descending. */ static final Ordering<WaveViewData> DEFAULT_ORDERING = DESC_LMT_ORDERING; /** Valid search query types. */ enum TokenQueryType { IN("in"), ORDERBY("orderby"), WITH("with"), CREATOR("creator"); final String token; TokenQueryType(String token) { this.token = token; } String getToken() { return token; } private static final Map<String, TokenQueryType> reverseLookupMap = new HashMap<String, TokenQueryType>(); static { for (TokenQueryType type : TokenQueryType.values()) { reverseLookupMap.put(type.getToken(), type); } } static TokenQueryType fromToken(String token) { TokenQueryType qyeryToken = reverseLookupMap.get(token); if (qyeryToken == null) { throw new IllegalArgumentException("Illegal query param: " + token); } return reverseLookupMap.get(token); } static boolean hasToken(String token) { return reverseLookupMap.keySet().contains(token); } } /** Registered order by parameter types and corresponding orderings. */ enum OrderByValueType { DATEASC("dateasc", ASC_LMT_ORDERING), DATEDESC("datedesc", DESC_LMT_ORDERING), CREATEDASC("createdasc", ASC_CREATED_ORDERING), CREATEDDESC("createddesc", DESC_CREATED_ORDERING), CREATORASC("creatorasc", ASC_CREATOR_ORDERING), CREATORDESC("creatordesc", DESC_CREATOR_ORDERING); final String value; final Ordering<WaveViewData> ordering; OrderByValueType(String value, Ordering<WaveViewData> ordering) { this.value = value; this.ordering = ordering; } String getToken() { return value; } Ordering<WaveViewData> getOrdering() { return ordering; } private static final Map<String, OrderByValueType> reverseLookupMap = new HashMap<String, OrderByValueType>(); static { for (OrderByValueType type : OrderByValueType.values()) { reverseLookupMap.put(type.getToken(), type); } } static OrderByValueType fromToken(String token) { OrderByValueType orderByValue = reverseLookupMap.get(token); if (orderByValue == null) { throw new IllegalArgumentException("Illegal 'orderby' value: " + token); } return reverseLookupMap.get(token); } } private QueryHelper() { } /** Static factory method. */ static QueryHelper newQueryHelper() { return new QueryHelper(); } /** * Parses the search query. * * @param query the query. * @return the result map with query tokens. Never returns null. * @throws InvalidQueryException if the query contains invalid params. */ Map<TokenQueryType, Set<String>> parseQuery(String query) throws InvalidQueryException { Preconditions.checkArgument(query != null); query = query.trim(); // If query is empty - return. if (query.isEmpty()) { return Collections.emptyMap(); } String[] tokens = query.split("\\s+"); Map<TokenQueryType, Set<String>> tokensMap = Maps.newEnumMap(TokenQueryType.class); for (String token : tokens) { String[] pair = token.split(":"); if (pair.length != 2 || !TokenQueryType.hasToken(pair[0])) { String msg = "Invalid query param: " + token; throw new InvalidQueryException(msg); } String tokenValue = pair[1]; TokenQueryType tokenType = TokenQueryType.fromToken(pair[0]); // Verify the orderby param. if (tokenType.equals(TokenQueryType.ORDERBY)) { try { OrderByValueType.fromToken(tokenValue); } catch (IllegalArgumentException e) { String msg = "Invalid orderby query value: " + tokenValue; throw new InvalidQueryException(msg); } } Set<String> valuesPerToken = tokensMap.get(tokenType); if (valuesPerToken == null) { valuesPerToken = Sets.newLinkedHashSet(); tokensMap.put(tokenType, valuesPerToken); } valuesPerToken.add(tokenValue); } return tokensMap; } /** * Builds a list of participants to serve as the filter for the query. * * @param queryParams the query params. * @param queryType the filter for the query , i.e. 'with'. * @param localDomain the local domain of the logged in user. * @return the participants list for the filter. * @throws InvalidParticipantAddress if participant id passed to the query is invalid. */ static List<ParticipantId> buildValidatedParticipantIds( Map<QueryHelper.TokenQueryType, Set<String>> queryParams, QueryHelper.TokenQueryType queryType, String localDomain) throws InvalidParticipantAddress { Set<String> tokenSet = queryParams.get(queryType); List<ParticipantId> participants = null; if (tokenSet != null) { participants = Lists.newArrayListWithCapacity(tokenSet.size()); for (String token : tokenSet) { if (!token.isEmpty() && token.indexOf("@") == -1) { // If no domain was specified, assume that the participant is from the local domain. token = token + "@" + localDomain; } else if (token.equals("@")) { // "@" is a shortcut for the shared domain participant. token = "@" + localDomain; } ParticipantId otherUser = ParticipantId.of(token); participants.add(otherUser); } } else { participants = Collections.emptyList(); } return participants; } /** * Computes ordering for the search results. If none are specified - then * returns the default ordering. The resulting ordering is always compounded * with ordering by wave id for stability. */ static Ordering<WaveViewData> computeSorter( Map<QueryHelper.TokenQueryType, Set<String>> queryParams) { Ordering<WaveViewData> ordering = null; Set<String> orderBySet = queryParams.get(QueryHelper.TokenQueryType.ORDERBY); if (orderBySet != null) { for (String orderBy : orderBySet) { QueryHelper.OrderByValueType orderingType = QueryHelper.OrderByValueType.fromToken(orderBy); if (ordering == null) { // Primary ordering. ordering = orderingType.getOrdering(); } else { // All other ordering are compounded to the primary one. ordering = ordering.compound(orderingType.getOrdering()); } } } else { ordering = QueryHelper.DEFAULT_ORDERING; } // For stability order also by wave id. ordering = ordering.compound(QueryHelper.ID_COMPARATOR); return ordering; } } private static final Log LOG = Log.get(WaveMap.class); private final QueryHelper queryHelper = QueryHelper.newQueryHelper(); /** * The wavelets in a wave. */ private static final class Wave implements Iterable<WaveletContainer> { private class WaveletCreator<T extends WaveletContainer> implements Function<WaveletId, T> { private final WaveletContainer.Factory<T> factory; private final String waveDomain; public WaveletCreator(WaveletContainer.Factory<T> factory, String waveDomain) { this.factory = factory; this.waveDomain = waveDomain; } @Override public T apply(WaveletId waveletId) { return factory.create(notifiee, WaveletName.of(waveId, waveletId), waveDomain); } } private final WaveId waveId; /** Future providing already-existing wavelets in storage. */ private final ListenableFuture<ImmutableSet<WaveletId>> lookedupWavelets; private final ConcurrentMap<WaveletId, LocalWaveletContainer> localWavelets; private final ConcurrentMap<WaveletId, RemoteWaveletContainer> remoteWavelets; private final WaveletNotificationSubscriber notifiee; /** * Creates a wave. The {@code lookupWavelets} future is examined only when a * query is first made. */ public Wave(WaveId waveId, ListenableFuture<ImmutableSet<WaveletId>> lookedupWavelets, WaveletNotificationSubscriber notifiee, LocalWaveletContainer.Factory localFactory, RemoteWaveletContainer.Factory remoteFactory, String waveDomain) { this.waveId = waveId; this.lookedupWavelets = lookedupWavelets; this.notifiee = notifiee; this.localWavelets = new MapMaker().makeComputingMap( new WaveletCreator<LocalWaveletContainer>(localFactory, waveDomain)); this.remoteWavelets = new MapMaker().makeComputingMap( new WaveletCreator<RemoteWaveletContainer>(remoteFactory, waveDomain)); } @Override public Iterator<WaveletContainer> iterator() { return Iterators.unmodifiableIterator( Iterables.concat(localWavelets.values(), remoteWavelets.values()).iterator()); } public LocalWaveletContainer getLocalWavelet(WaveletId waveletId) throws WaveletStateException { return getWavelet(waveletId, localWavelets); } public RemoteWaveletContainer getRemoteWavelet(WaveletId waveletId) throws WaveletStateException { return getWavelet(waveletId, remoteWavelets); } public LocalWaveletContainer getOrCreateLocalWavelet(WaveletId waveletId) { return localWavelets.get(waveletId); } public RemoteWaveletContainer getOrCreateRemoteWavelet(WaveletId waveletId) { return remoteWavelets.get(waveletId); } private <T extends WaveletContainer> T getWavelet(WaveletId waveletId, ConcurrentMap<WaveletId, T> waveletsMap) throws WaveletStateException { ImmutableSet<WaveletId> storedWavelets; try { storedWavelets = FutureUtil.getResultOrPropagateException(lookedupWavelets, PersistenceException.class); } catch (PersistenceException e) { throw new WaveletStateException( "Failed to lookup wavelet " + WaveletName.of(waveId, waveletId), e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new WaveletStateException( "Interrupted looking up wavelet " + WaveletName.of(waveId, waveletId), e); } // Since waveletsMap is a computing map, we must call containsKey(waveletId) // to tell if waveletId is mapped, we cannot test if get(waveletId) returns null. if (!storedWavelets.contains(waveletId) && !waveletsMap.containsKey(waveletId)) { return null; } else { T wavelet = waveletsMap.get(waveletId); Preconditions.checkNotNull(wavelet, "computingMap returned null"); return wavelet; } } } /** * Returns a future whose result is the ids of stored wavelets in the given wave. * Any failure is reported as a {@link PersistenceException}. */ private static ListenableFuture<ImmutableSet<WaveletId>> lookupWavelets( final WaveId waveId, final WaveletStore<?> waveletStore, Executor lookupExecutor) { ListenableFutureTask<ImmutableSet<WaveletId>> task = new ListenableFutureTask<ImmutableSet<WaveletId>>( new Callable<ImmutableSet<WaveletId>>() { @Override public ImmutableSet<WaveletId> call() throws PersistenceException { return waveletStore.lookup(waveId); } }); lookupExecutor.execute(task); return task; } /** * The period of time in minutes the per user waves view should be actively * kept up to date after last access. */ private static final int PER_USER_WAVES_VIEW_CACHE_TIME = 5; private final ConcurrentMap<WaveId, Wave> waves; private final WaveletStore<?> store; /** The computing map that holds wave viev per each online user.*/ private final ConcurrentMap<ParticipantId, Multimap<WaveId, WaveletId>> explicitPerUserWaveViews; private final ParticipantId sharedDomainParticipantId; private final Subscriber subscriber = new Subscriber() { @Override public void waveletUpdate(ReadableWaveletData wavelet, DeltaSequence deltas) { WaveletId waveletId = wavelet.getWaveletId(); if (IdUtil.isUserDataWavelet(waveletId)) { return; } // Find whether participants where added/removed and update the views // accordingly. for (TransformedWaveletDelta delta : deltas) { for (WaveletOperation op : delta) { if (op instanceof AddParticipant) { ParticipantId user = ((AddParticipant) op).getParticipantId(); // Check first if we need to update views for this user. if (explicitPerUserWaveViews.containsKey(user)) { Multimap<WaveId, WaveletId> perUserView = explicitPerUserWaveViews.get(user); WaveId waveId = wavelet.getWaveId(); if (!perUserView.containsEntry(waveId, waveletId)) { perUserView.put(waveId, waveletId); LOG.fine("Added wavelet: " + WaveletName.of(waveId, waveletId) + " to the view of user: " + user.getAddress()); } } } else if (op instanceof RemoveParticipant) { ParticipantId user = ((RemoveParticipant) op).getParticipantId(); if (explicitPerUserWaveViews.containsKey(user)) { Multimap<WaveId, WaveletId> perUserView = explicitPerUserWaveViews.get(user); WaveId waveId = wavelet.getWaveId(); if (perUserView.containsEntry(waveId, waveletId)) { perUserView.remove(waveId, waveletId); LOG.fine("Removed walet: " + WaveletName.of(waveId, waveletId) + " from the view of user: " + user.getAddress()); } } } } } } @Override public void waveletCommitted(WaveletName waveletName, HashedVersion version) { // No op. } }; @Inject public WaveMap(final DeltaAndSnapshotStore waveletStore, final WaveletNotificationSubscriber notifiee, WaveBus dispatcher, final LocalWaveletContainer.Factory localFactory, final RemoteWaveletContainer.Factory remoteFactory, @Named(CoreSettings.WAVE_SERVER_DOMAIN) final String waveDomain) { // NOTE(anorth): DeltaAndSnapshotStore is more specific than necessary, but // helps Guice out. // TODO(soren): inject a proper executor (with a pool of configurable size) this.store = waveletStore; sharedDomainParticipantId = ParticipantIdUtil.makeUnsafeSharedDomainParticipantId(waveDomain); dispatcher.subscribe(subscriber); final Executor lookupExecutor = Executors.newSingleThreadExecutor(); waves = new MapMaker().makeComputingMap(new Function<WaveId, Wave>() { @Override public Wave apply(WaveId waveId) { ListenableFuture<ImmutableSet<WaveletId>> lookedupWavelets = lookupWavelets(waveId, waveletStore, lookupExecutor); return new Wave(waveId, lookedupWavelets, notifiee, localFactory, remoteFactory, waveDomain); } }); // Let the view expire if it not accessed for some time. explicitPerUserWaveViews = new MapMaker().expireAfterAccess(PER_USER_WAVES_VIEW_CACHE_TIME, TimeUnit.MINUTES) .makeComputingMap(new Function<ParticipantId, Multimap<WaveId, WaveletId>>() { @Override public Multimap<WaveId, WaveletId> apply(final ParticipantId user) { Multimap<WaveId, WaveletId> userView = HashMultimap.create(); // Create initial per user waves view by looping over all waves // in the waves store. // After that the view is maintained up to date continuously in // the subscriber.waveletUpdate method until the user logs of // and the key is expired. // On the next login the waves view will be rebuild. for (Map.Entry<WaveId, Wave> entry : waves.entrySet()) { Wave wave = entry.getValue(); for (WaveletContainer c : wave) { WaveletId waveletId = c.getWaveletName().waveletId; try { if (IdUtil.isUserDataWavelet(waveletId) || !c.hasParticipant(user)) { continue; } // Add this wave to the user view. userView.put(entry.getKey(), waveletId); } catch (WaveletStateException e) { LOG.warning("Failed to access wavelet " + c.getWaveletName(), e); } } } LOG.info("Initalized waves view for user: " + user.getAddress() + ", number of waves in view: " + userView.size()); return userView; } }); } /** * Loads all wavelets from storage. * * @throws WaveletStateException if storage access fails. */ public void loadAllWavelets() throws WaveletStateException { try { ExceptionalIterator<WaveId, PersistenceException> itr = store.getWaveIdIterator(); while (itr.hasNext()) { WaveId waveId = itr.next(); lookupWavelets(waveId); } } catch (PersistenceException e) { throw new WaveletStateException("Failed to scan waves", e); } } @Override public Collection<WaveViewData> search(final ParticipantId user, String query, int startAt, int numResults) { LOG.fine("Search query '" + query + "' from user: " + user + " [" + startAt + ", " + (startAt + numResults - 1) + "]"); Map<QueryHelper.TokenQueryType, Set<String>> queryParams = null; try { queryParams = queryHelper.parseQuery(query); } catch (QueryHelper.InvalidQueryException e1) { // Invalid query param - stop and return empty search results. LOG.warning("Invalid Query. " + e1.getMessage()); return Collections.emptyList(); } final List<ParticipantId> withParticipantIds; final List<ParticipantId> creatorParticipantIds; try { String localDomain = user.getDomain(); // Build and validate. withParticipantIds = QueryHelper.buildValidatedParticipantIds(queryParams, QueryHelper.TokenQueryType.WITH, localDomain); creatorParticipantIds = QueryHelper.buildValidatedParticipantIds(queryParams, QueryHelper.TokenQueryType.CREATOR, localDomain); } catch (InvalidParticipantAddress e) { // Invalid address - stop and return empty search results. LOG.warning("Invalid participantId: " + e.getAddress() + " in query: " + query); return Collections.emptyList(); } // Maybe should be changed in case other folders in addition to 'inbox' are // added. final boolean isAllQuery = !queryParams.containsKey(QueryHelper.TokenQueryType.IN); // A function to be applied by the WaveletContainer. Function<ReadableWaveletData, Boolean> matchesFunction = new Function<ReadableWaveletData, Boolean>() { @Override public Boolean apply(ReadableWaveletData wavelet) { try { return matches(wavelet, user, sharedDomainParticipantId, withParticipantIds, creatorParticipantIds, isAllQuery); } catch (WaveletStateException e) { LOG.warning( "Failed to access wavelet " + WaveletName.of(wavelet.getWaveId(), wavelet.getWaveletId()), e); return false; } } }; Multimap<WaveId, WaveletId> currentUserWavesView; if (isAllQuery) { // If it is the "all" query - we need to include also waves view of the // shared domain participant. currentUserWavesView = HashMultimap.create(); currentUserWavesView.putAll(explicitPerUserWaveViews.get(user)); currentUserWavesView.putAll(explicitPerUserWaveViews.get(sharedDomainParticipantId)); } else { currentUserWavesView = explicitPerUserWaveViews.get(user); } // 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()) { Wave wave = waves.get(waveId); WaveViewData view = null; // Copy of the wave built up for search hits. for (WaveletContainer c : wave) { // 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 (!c.applyFunction(matchesFunction)) { continue; } if (view == null) { view = WaveViewDataImpl.create(waveId); } // Just keep adding all the relevant wavelets in this wave. view.addWavelet(c.copyWaveletData()); } catch (WaveletStateException e) { LOG.warning("Failed to access wavelet " + c.getWaveletName(), e); } } // Filter out waves without conversational root wavelet from search result. if (view != null && WaveletDataUtil.hasConversationalRootWavelet(view)) { results.put(waveId, view); } } 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); } LOG.info("Search response to '" + query + "': " + searchResultslist.size() + " results, user: " + user); // Memory management wise it's dangerous to return a sublist of a much // longer list, therefore, we return a 'defensive' copy. return ImmutableList.copyOf(searchResultslist); } /** * 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 matches(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; } public ExceptionalIterator<WaveId, WaveServerException> getWaveIds() { Iterator<WaveId> inner = waves.keySet().iterator(); return ExceptionalIterator.FromIterator.create(inner); } public ImmutableSet<WaveletId> lookupWavelets(WaveId waveId) throws WaveletStateException { ListenableFuture<ImmutableSet<WaveletId>> future = waves.get(waveId).lookedupWavelets; try { return FutureUtil.getResultOrPropagateException(future, PersistenceException.class); } catch (PersistenceException e) { throw new WaveletStateException("Failed to look up wave " + waveId, e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new WaveletStateException("Interrupted while looking up wave " + waveId, e); } } public LocalWaveletContainer getLocalWavelet(WaveletName waveletName) throws WaveletStateException { return waves.get(waveletName.waveId).getLocalWavelet(waveletName.waveletId); } public RemoteWaveletContainer getRemoteWavelet(WaveletName waveletName) throws WaveletStateException { return waves.get(waveletName.waveId).getRemoteWavelet(waveletName.waveletId); } public LocalWaveletContainer getOrCreateLocalWavelet(WaveletName waveletName) { return waves.get(waveletName.waveId).getOrCreateLocalWavelet(waveletName.waveletId); } public RemoteWaveletContainer getOrCreateRemoteWavelet(WaveletName waveletName) { return waves.get(waveletName.waveId).getOrCreateRemoteWavelet(waveletName.waveletId); } }