/**************************************************************** | |
* 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.apache.james.mailbox.store.search; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.EnumSet; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.SortedSet; | |
import java.util.TreeSet; | |
import javax.inject.Inject; | |
import org.apache.james.mailbox.MailboxManager.SearchCapabilities; | |
import org.apache.james.mailbox.MailboxSession; | |
import org.apache.james.mailbox.exception.MailboxException; | |
import org.apache.james.mailbox.model.MailboxId; | |
import org.apache.james.mailbox.model.MailboxPath; | |
import org.apache.james.mailbox.model.MessageRange; | |
import org.apache.james.mailbox.model.MultimailboxesSearchQuery; | |
import org.apache.james.mailbox.model.SearchQuery; | |
import org.apache.james.mailbox.model.SearchQuery.ConjunctionCriterion; | |
import org.apache.james.mailbox.model.SearchQuery.Criterion; | |
import org.apache.james.mailbox.model.SearchQuery.NumericRange; | |
import org.apache.james.mailbox.model.SearchQuery.UidCriterion; | |
import org.apache.james.mailbox.store.mail.MailboxMapperFactory; | |
import org.apache.james.mailbox.store.mail.MessageMapper; | |
import org.apache.james.mailbox.store.mail.MessageMapper.FetchType; | |
import org.apache.james.mailbox.store.mail.MessageMapperFactory; | |
import org.apache.james.mailbox.store.mail.model.Mailbox; | |
import org.apache.james.mailbox.store.mail.model.MailboxMessage; | |
import com.google.common.base.Preconditions; | |
import com.google.common.base.Predicate; | |
import com.google.common.base.Predicates; | |
import com.google.common.collect.FluentIterable; | |
import com.google.common.collect.ImmutableList; | |
import com.google.common.collect.ImmutableMultimap; | |
import com.google.common.collect.ImmutableMultimap.Builder; | |
import com.google.common.collect.Multimap; | |
/** | |
* {@link MessageSearchIndex} which just fetch {@link MailboxMessage}'s from the {@link MessageMapper} and use {@link MessageSearcher} | |
* to match them against the {@link SearchQuery}. | |
* | |
* This works with every implementation but is SLOW. | |
* | |
* | |
*/ | |
public class SimpleMessageSearchIndex implements MessageSearchIndex { | |
private static final String WILDCARD = "%"; | |
private final MessageMapperFactory messageMapperFactory; | |
private final MailboxMapperFactory mailboxMapperFactory; | |
@Inject | |
public SimpleMessageSearchIndex(MessageMapperFactory messageMapperFactory, MailboxMapperFactory mailboxMapperFactory) { | |
this.messageMapperFactory = messageMapperFactory; | |
this.mailboxMapperFactory = mailboxMapperFactory; | |
} | |
@Override | |
public EnumSet<SearchCapabilities> getSupportedCapabilities() { | |
return EnumSet.of(SearchCapabilities.MultimailboxSearch, SearchCapabilities.Text); | |
} | |
/** | |
* Walks down the query tree's conjunctions to find a UidCriterion | |
* @param crits - list of Criterion to search from | |
* @return | |
* first UidCriterion found | |
* null - if not found | |
*/ | |
private static UidCriterion findConjugatedUidCriterion(List<Criterion> crits) { | |
for (Criterion crit : crits) { | |
if (crit instanceof UidCriterion) { | |
return (UidCriterion) crit; | |
} else if (crit instanceof ConjunctionCriterion) { | |
return findConjugatedUidCriterion(((ConjunctionCriterion) crit) | |
.getCriteria()); | |
} | |
} | |
return null; | |
} | |
@Override | |
public Iterator<Long> search(MailboxSession session, Mailbox mailbox, SearchQuery query) throws MailboxException { | |
Preconditions.checkArgument(session != null, "'session' is mandatory"); | |
return searchMultimap(session, ImmutableList.of(mailbox), query) | |
.get(mailbox.getMailboxId()) | |
.iterator(); | |
} | |
private Multimap<MailboxId, Long> searchMultimap(MailboxSession session, Iterable<Mailbox> mailboxes, SearchQuery query) throws MailboxException { | |
Builder<MailboxId, Long> multimap = ImmutableMultimap.builder(); | |
for (Mailbox mailbox: mailboxes) { | |
multimap.putAll(searchMultimap(session, mailbox, query)); | |
} | |
return multimap.build(); | |
} | |
private Multimap<MailboxId, Long> searchMultimap(MailboxSession session, Mailbox mailbox, SearchQuery query) throws MailboxException { | |
if (!isMatchingUser(session, mailbox)) { | |
return ImmutableMultimap.of(); | |
} | |
MessageMapper mapper = messageMapperFactory.getMessageMapper(session); | |
final SortedSet<MailboxMessage> hitSet = new TreeSet<MailboxMessage>(); | |
UidCriterion uidCrit = findConjugatedUidCriterion(query.getCriterias()); | |
if (uidCrit != null) { | |
// if there is a conjugated uid range criterion in the query tree we can optimize by | |
// only fetching this uid range | |
NumericRange[] ranges = uidCrit.getOperator().getRange(); | |
for (NumericRange r : ranges) { | |
Iterator<MailboxMessage> it = mapper.findInMailbox(mailbox, MessageRange.range(r.getLowValue(), r.getHighValue()), FetchType.Metadata, -1); | |
while (it.hasNext()) { | |
hitSet.add(it.next()); | |
} | |
} | |
} else { | |
// we have to fetch all messages | |
Iterator<MailboxMessage> messages = mapper.findInMailbox(mailbox, MessageRange.all(), FetchType.Full, -1); | |
while(messages.hasNext()) { | |
MailboxMessage m = messages.next(); | |
hitSet.add(m); | |
} | |
} | |
// MessageSearches does the filtering for us | |
return ImmutableMultimap.<MailboxId, Long>builder() | |
.putAll(mailbox.getMailboxId(), ImmutableList.copyOf(new MessageSearches(hitSet.iterator(), query, session).iterator())) | |
.build(); | |
} | |
private boolean isMatchingUser(MailboxSession session, Mailbox mailbox) { | |
return mailbox.getUser().equals(session.getUser().getUserName()); | |
} | |
@Override | |
public Map<MailboxId, Collection<Long>> search(MailboxSession session, final MultimailboxesSearchQuery searchQuery) throws MailboxException { | |
List<Mailbox> allUserMailboxes = mailboxMapperFactory.getMailboxMapper(session) | |
.findMailboxWithPathLike(new MailboxPath(session.getPersonalSpace(), session.getUser().getUserName(), WILDCARD)); | |
FluentIterable<Mailbox> filteredMailboxes = FluentIterable.from(allUserMailboxes).filter(new Predicate<Mailbox>() { | |
@Override | |
public boolean apply(Mailbox input) { | |
return !searchQuery.getNotInMailboxes().contains(input.getMailboxId()); | |
} | |
}); | |
if (searchQuery.getInMailboxes().isEmpty()) { | |
return searchMultimap(session, filteredMailboxes, searchQuery.getSearchQuery()) | |
.asMap(); | |
} | |
List<Mailbox> queriedMailboxes = new ArrayList<Mailbox>(); | |
for (Mailbox mailbox: filteredMailboxes) { | |
if (searchQuery.getInMailboxes().contains(mailbox.getMailboxId())) { | |
queriedMailboxes.add(mailbox); | |
} | |
} | |
return searchMultimap(session, queriedMailboxes, searchQuery.getSearchQuery()) | |
.asMap(); | |
} | |
} |