/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import org.waveprotocol.wave.model.id.IdUtil; import org.waveprotocol.wave.model.wave.InvalidParticipantAddress; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveViewData; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Helper class that allows to add basic sort and filter functionality to the * search. * * @author yurize@apache.org (Yuri Zelikov) */ public class QueryHelper { @SuppressWarnings("serial") public static 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 (Last Modified Time). */ 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 (Last Modified Time). */ public 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. */ public 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. */ public 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 */ public 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 */ public 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. */ public 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}. */ public static final Ordering<WaveViewData> ASC_LMT_ORDERING = Ordering .from(QueryHelper.ASC_LMT_COMPARATOR); /** * Orders using {@link DESCENDING_DATE_COMPARATOR}. */ public static final Ordering<WaveViewData> DESC_LMT_ORDERING = Ordering .from(QueryHelper.DESC_LMT_COMPARATOR); /** * Orders using {@link ASC_CREATED_COMPARATOR}. */ public static final Ordering<WaveViewData> ASC_CREATED_ORDERING = Ordering .from(QueryHelper.ASC_CREATED_COMPARATOR); /** * Orders using {@link DESC_CREATED_COMPARATOR}. */ public static final Ordering<WaveViewData> DESC_CREATED_ORDERING = Ordering .from(QueryHelper.DESC_CREATED_COMPARATOR); /** * Orders using {@link ASC_CREATOR_COMPARATOR}. */ public static final Ordering<WaveViewData> ASC_CREATOR_ORDERING = Ordering .from(QueryHelper.ASC_CREATOR_COMPARATOR); /** * Orders using {@link DESC_CREATOR_COMPARATOR}. */ public static final Ordering<WaveViewData> DESC_CREATOR_ORDERING = Ordering .from(QueryHelper.DESC_CREATOR_COMPARATOR); /** Default ordering is by LMT descending. */ public static final Ordering<WaveViewData> DEFAULT_ORDERING = DESC_LMT_ORDERING; /** Registered order by parameter types and corresponding orderings. */ public 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 token; final Ordering<WaveViewData> ordering; OrderByValueType(String value, Ordering<WaveViewData> ordering) { this.token = value; this.ordering = ordering; } public String getToken() { return token; } public 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); } } public static OrderByValueType fromToken(String token) { OrderByValueType orderByValue = reverseLookupMap.get(token); if (orderByValue == null) { throw new IllegalArgumentException("Illegal 'orderby' value: " + token); } return reverseLookupMap.get(token); } } /** * 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. */ public static List<ParticipantId> buildValidatedParticipantIds( Map<TokenQueryType, Set<String>> queryParams, 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. */ public static Ordering<WaveViewData> computeSorter( Map<TokenQueryType, Set<String>> queryParams) { Ordering<WaveViewData> ordering = null; Set<String> orderBySet = queryParams.get(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; } /** * 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. */ public static 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; } /** Private constructor to prevent instantiation. */ private QueryHelper() {} }