blob: bc486058be40c37651af41c20ee1e43cc377817a [file] [log] [blame]
/****************************************************************
* 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.lucene.search;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import javax.mail.Flags;
import javax.mail.Flags.Flag;
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.exception.UnsupportedSearchException;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxId.Factory;
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.AllCriterion;
import org.apache.james.mailbox.model.SearchQuery.ContainsOperator;
import org.apache.james.mailbox.model.SearchQuery.Criterion;
import org.apache.james.mailbox.model.SearchQuery.CustomFlagCriterion;
import org.apache.james.mailbox.model.SearchQuery.DateOperator;
import org.apache.james.mailbox.model.SearchQuery.DateResolution;
import org.apache.james.mailbox.model.SearchQuery.FlagCriterion;
import org.apache.james.mailbox.model.SearchQuery.HeaderCriterion;
import org.apache.james.mailbox.model.SearchQuery.HeaderOperator;
import org.apache.james.mailbox.model.SearchQuery.NumericOperator;
import org.apache.james.mailbox.model.SearchQuery.NumericRange;
import org.apache.james.mailbox.model.SearchQuery.UidCriterion;
import org.apache.james.mailbox.model.UpdatedFlags;
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 org.apache.james.mailbox.store.search.ListeningMessageSearchIndex;
import org.apache.james.mailbox.store.search.SearchUtil;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.Header;
import org.apache.james.mime4j.dom.address.Address;
import org.apache.james.mime4j.dom.address.AddressList;
import org.apache.james.mime4j.dom.address.Group;
import org.apache.james.mime4j.dom.address.MailboxList;
import org.apache.james.mime4j.dom.datetime.DateTime;
import org.apache.james.mime4j.dom.field.DateTimeField;
import org.apache.james.mime4j.field.address.AddressFormatter;
import org.apache.james.mime4j.field.address.LenientAddressParser;
import org.apache.james.mime4j.field.datetime.parser.DateTimeParser;
import org.apache.james.mime4j.message.SimpleContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.util.MimeUtil;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Index;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.NumericField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.Version;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
/**
* Lucene based {@link ListeningMessageSearchIndex} which offers message searching via a Lucene index
*
*
* @param
*/
public class LuceneMessageSearchIndex extends ListeningMessageSearchIndex {
private final static Date MAX_DATE;
private final static Date MIN_DATE;
static {
Calendar cal = Calendar.getInstance();
cal.set(9999, 11, 31);
MAX_DATE = cal.getTime();
cal.set(0000, 0, 1);
MIN_DATE = cal.getTime();
}
/**
* Default max query results
*/
public final static int DEFAULT_MAX_QUERY_RESULTS = 100000;
/**
* {@link Field} which will contain the unique index of the {@link Document}
*/
public final static String ID_FIELD ="id";
/**
* {@link Field} which will contain uid of the {@link MailboxMessage}
*/
public final static String UID_FIELD = "uid";
/**
* {@link Field} which will contain the {@link Flags} of the {@link MailboxMessage}
*/
public final static String FLAGS_FIELD = "flags";
/**
* {@link Field} which will contain the size of the {@link MailboxMessage}
*/
public final static String SIZE_FIELD = "size";
/**
* {@link Field} which will contain the body of the {@link MailboxMessage}
*/
public final static String BODY_FIELD = "body";
/**
* Prefix which will be used for each message header to store it also in a seperate {@link Field}
*/
public final static String PREFIX_HEADER_FIELD ="header_";
/**
* {@link Field} which will contain the whole message header of the {@link MailboxMessage}
*/
public final static String HEADERS_FIELD ="headers";
/**
* {@link Field} which will contain the mod-sequence of the message
*/
public final static String MODSEQ_FIELD = "modSeq";
/**
* {@link Field} which will contain the TO-Address of the message
*/
public final static String TO_FIELD ="to";
public final static String FIRST_TO_MAILBOX_NAME_FIELD ="firstToMailboxName";
public final static String FIRST_TO_MAILBOX_DISPLAY_FIELD ="firstToMailboxDisplay";
/**
* {@link Field} which will contain the CC-Address of the message
*/
public final static String CC_FIELD ="cc";
public final static String FIRST_CC_MAILBOX_NAME_FIELD ="firstCcMailboxName";
/**
* {@link Field} which will contain the FROM-Address of the message
*/
public final static String FROM_FIELD ="from";
public final static String FIRST_FROM_MAILBOX_NAME_FIELD ="firstFromMailboxName";
public final static String FIRST_FROM_MAILBOX_DISPLAY_FIELD ="firstFromMailboxDisplay";
/**
* {@link Field} which will contain the BCC-Address of the message
*/
public final static String BCC_FIELD ="bcc";
public final static String BASE_SUBJECT_FIELD = "baseSubject";
/**
* {@link Field} which contain the internalDate of the message with YEAR-Resolution
*/
public final static String INTERNAL_DATE_FIELD_YEAR_RESOLUTION ="internaldateYearResolution";
/**
* {@link Field} which contain the internalDate of the message with MONTH-Resolution
*/
public final static String INTERNAL_DATE_FIELD_MONTH_RESOLUTION ="internaldateMonthResolution";
/**
* {@link Field} which contain the internalDate of the message with DAY-Resolution
*/
public final static String INTERNAL_DATE_FIELD_DAY_RESOLUTION ="internaldateDayResolution";
/**
* {@link Field} which contain the internalDate of the message with HOUR-Resolution
*/
public final static String INTERNAL_DATE_FIELD_HOUR_RESOLUTION ="internaldateHourResolution";
/**
* {@link Field} which contain the internalDate of the message with MINUTE-Resolution
*/
public final static String INTERNAL_DATE_FIELD_MINUTE_RESOLUTION ="internaldateMinuteResolution";
/**
* {@link Field} which contain the internalDate of the message with SECOND-Resolution
*/
public final static String INTERNAL_DATE_FIELD_SECOND_RESOLUTION ="internaldateSecondResolution";
/**
* {@link Field} which contain the internalDate of the message with MILLISECOND-Resolution
*/
public final static String INTERNAL_DATE_FIELD_MILLISECOND_RESOLUTION ="internaldateMillisecondResolution";
/**
* {@link Field} which will contain the id of the {@link Mailbox}
*/
public final static String MAILBOX_ID_FIELD ="mailboxid";
/**
* {@link Field} which contain the Date header of the message with YEAR-Resolution
*/
public final static String SENT_DATE_FIELD_YEAR_RESOLUTION ="sentdateYearResolution";
/**
* {@link Field} which contain the Date header of the message with MONTH-Resolution
*/
public final static String SENT_DATE_FIELD_MONTH_RESOLUTION ="sentdateMonthResolution";
/**
* {@link Field} which contain the Date header of the message with DAY-Resolution
*/
public final static String SENT_DATE_FIELD_DAY_RESOLUTION ="sentdateDayResolution";
/**
* {@link Field} which contain the Date header of the message with HOUR-Resolution
*/
public final static String SENT_DATE_FIELD_HOUR_RESOLUTION ="sentdateHourResolution";
/**
* {@link Field} which contain the Date header of the message with MINUTE-Resolution
*/
public final static String SENT_DATE_FIELD_MINUTE_RESOLUTION ="sentdateMinuteResolution";
/**
* {@link Field} which contain the Date header of the message with SECOND-Resolution
*/
public final static String SENT_DATE_FIELD_SECOND_RESOLUTION ="sentdateSecondResolution";
/**
* {@link Field} which contain the Date header of the message with MILLISECOND-Resolution
*/
public final static String SENT_DATE_FIELD_MILLISECOND_RESOLUTION ="sentdateMillisecondResolution";
public final static String SENT_DATE_SORT_FIELD_MILLISECOND_RESOLUTION ="sentdateSort";
public final static String NON_EXIST_FIELD ="nonExistField";
private final static String MEDIA_TYPE_TEXT = "text";
private final static String MEDIA_TYPE_MESSAGE = "message";
private final static String DEFAULT_ENCODING = "US-ASCII";
private final static SortField UID_SORT = new SortField(UID_FIELD, SortField.LONG);
private final static SortField UID_SORT_REVERSE = new SortField(UID_FIELD, SortField.LONG, true);
private final static SortField SIZE_SORT = new SortField(SIZE_FIELD, SortField.LONG);
private final static SortField SIZE_SORT_REVERSE = new SortField(SIZE_FIELD, SortField.LONG, true);
private final static SortField FIRST_CC_MAILBOX_SORT = new SortField(FIRST_CC_MAILBOX_NAME_FIELD, SortField.STRING);
private final static SortField FIRST_CC_MAILBOX_SORT_REVERSE = new SortField(FIRST_CC_MAILBOX_NAME_FIELD, SortField.STRING, true);
private final static SortField FIRST_TO_MAILBOX_SORT = new SortField(FIRST_TO_MAILBOX_NAME_FIELD, SortField.STRING);
private final static SortField FIRST_TO_MAILBOX_SORT_REVERSE = new SortField(FIRST_TO_MAILBOX_NAME_FIELD, SortField.STRING, true);
private final static SortField FIRST_FROM_MAILBOX_SORT = new SortField(FIRST_FROM_MAILBOX_NAME_FIELD, SortField.STRING);
private final static SortField FIRST_FROM_MAILBOX_SORT_REVERSE = new SortField(FIRST_FROM_MAILBOX_NAME_FIELD, SortField.STRING, true);
private final static SortField ARRIVAL_MAILBOX_SORT = new SortField(INTERNAL_DATE_FIELD_MILLISECOND_RESOLUTION, SortField.LONG);
private final static SortField ARRIVAL_MAILBOX_SORT_REVERSE = new SortField(INTERNAL_DATE_FIELD_MILLISECOND_RESOLUTION, SortField.LONG, true);
private final static SortField BASE_SUBJECT_SORT = new SortField(BASE_SUBJECT_FIELD, SortField.STRING);
private final static SortField BASE_SUBJECT_SORT_REVERSE = new SortField(BASE_SUBJECT_FIELD, SortField.STRING, true);
private final static SortField SENT_DATE_SORT = new SortField(SENT_DATE_SORT_FIELD_MILLISECOND_RESOLUTION, SortField.LONG);
private final static SortField SENT_DATE_SORT_REVERSE = new SortField(SENT_DATE_SORT_FIELD_MILLISECOND_RESOLUTION, SortField.LONG, true);
private final static SortField FIRST_TO_MAILBOX_DISPLAY_SORT = new SortField(FIRST_TO_MAILBOX_DISPLAY_FIELD, SortField.STRING);
private final static SortField FIRST_TO_MAILBOX_DISPLAY_SORT_REVERSE = new SortField(FIRST_TO_MAILBOX_DISPLAY_FIELD, SortField.STRING, true);
private final static SortField FIRST_FROM_MAILBOX_DISPLAY_SORT = new SortField(FIRST_FROM_MAILBOX_DISPLAY_FIELD, SortField.STRING);
private final static SortField FIRST_FROM_MAILBOX_DISPLAY_SORT_REVERSE = new SortField(FIRST_FROM_MAILBOX_DISPLAY_FIELD, SortField.STRING, true);
private final Factory mailboxIdFactory;
private final IndexWriter writer;
private int maxQueryResults = DEFAULT_MAX_QUERY_RESULTS;
private boolean suffixMatch = false;
public LuceneMessageSearchIndex(MessageMapperFactory factory, MailboxId.Factory mailboxIdFactory, Directory directory) throws CorruptIndexException, LockObtainFailedException, IOException {
this(factory, mailboxIdFactory, directory, false, true);
}
public LuceneMessageSearchIndex(MessageMapperFactory factory, MailboxId.Factory mailboxIdFactory, Directory directory, boolean dropIndexOnStart, boolean lenient) throws CorruptIndexException, LockObtainFailedException, IOException {
super(factory);
this.mailboxIdFactory = mailboxIdFactory;
this.writer = new IndexWriter(directory, createConfig(createAnalyzer(lenient), dropIndexOnStart));
}
public LuceneMessageSearchIndex(MessageMapperFactory factory, MailboxId.Factory mailboxIdFactory, IndexWriter writer) {
super(factory);
this.mailboxIdFactory = mailboxIdFactory;
this.writer = writer;
}
@Override
public ListenerType getType() {
return ListenerType.EACH_NODE;
}
@Override
public EnumSet<SearchCapabilities> getSupportedCapabilities() {
return EnumSet.noneOf(SearchCapabilities.class);
}
/**
* Set the max count of results which will get returned from a query. The default is {@link #DEFAULT_MAX_QUERY_RESULTS}
*
* @param maxQueryResults
*/
public void setMaxQueryResults(int maxQueryResults) {
this.maxQueryResults = maxQueryResults;
}
protected IndexWriterConfig createConfig(Analyzer analyzer, boolean dropIndexOnStart) {
IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_31, analyzer);
if (dropIndexOnStart) {
config.setOpenMode(OpenMode.CREATE);
} else {
config.setOpenMode(OpenMode.CREATE_OR_APPEND);
}
return config;
}
/**
* Create a {@link Analyzer} which is used to index the {@link MailboxMessage}'s
*
* @param lenient
*
* @return analyzer
*/
protected Analyzer createAnalyzer(boolean lenient) {
if (lenient) {
return new LenientImapSearchAnalyzer();
} else {
return new StrictImapSearchAnalyzer();
}
}
/**
* If set to true this implementation will use {@link WildcardQuery} to match suffix and prefix. This is what RFC3501 expects but is often not what the user does.
* It also slow things a lot if you have complex queries which use many "TEXT" arguments. If you want the implementation to behave strict like RFC3501 says, you should
* set this to true.
*
* The default is false for performance reasons
*
*
* @param suffixMatch
*/
public void setEnableSuffixMatch(boolean suffixMatch) {
this.suffixMatch = suffixMatch;
}
/**
* @see org.apache.james.mailbox.store.search.MessageSearchIndex#search(org.apache.james.mailbox.MailboxSession, org.apache.james.mailbox.store.mail.model.Mailbox, org.apache.james.mailbox.model.SearchQuery)
*/
public Iterator<Long> search(MailboxSession session, Mailbox mailbox, SearchQuery searchQuery) throws MailboxException {
Preconditions.checkArgument(session != null, "'session' is mandatory");
MailboxId mailboxId = mailbox.getMailboxId();
Multimap<MailboxId, Long> results =
searchMultimap(
session,
MultimailboxesSearchQuery
.from(searchQuery)
.inMailboxes(mailboxId)
.build());
return results.get(mailboxId).iterator();
}
@Override
public Map<MailboxId, Collection<Long>> search(MailboxSession session, MultimailboxesSearchQuery searchQuery) throws MailboxException {
Preconditions.checkArgument(session != null, "'session' is mandatory");
return searchMultimap(session, searchQuery).asMap();
}
private Multimap<MailboxId, Long> searchMultimap(MailboxSession session, MultimailboxesSearchQuery searchQuery) throws MailboxException {
Multimap<MailboxId, Long> results = LinkedHashMultimap.create();
IndexSearcher searcher = null;
Query inMailboxes = buildQueryFromMailboxes(searchQuery.getMailboxIds());
try {
searcher = new IndexSearcher(IndexReader.open(writer, true));
BooleanQuery query = new BooleanQuery();
query.add(inMailboxes, BooleanClause.Occur.MUST);
// Not return flags documents
query.add(new PrefixQuery(new Term(FLAGS_FIELD, "")), BooleanClause.Occur.MUST_NOT);
List<Criterion> crits = searchQuery.getSearchQuery().getCriterias();
for (Criterion crit : crits) {
query.add(createQuery(crit, inMailboxes, searchQuery.getSearchQuery().getRecentMessageUids()), BooleanClause.Occur.MUST);
}
// query for all the documents sorted as specified in the SearchQuery
TopDocs docs = searcher.search(query, null, maxQueryResults, createSort(searchQuery.getSearchQuery().getSorts()));
ScoreDoc[] sDocs = docs.scoreDocs;
for (ScoreDoc sDoc : sDocs) {
Document doc = searcher.doc(sDoc.doc);
long uid = Long.valueOf(doc.get(UID_FIELD));
MailboxId mailboxId = mailboxIdFactory.fromString(doc.get(MAILBOX_ID_FIELD));
results.put(mailboxId, uid);
}
} catch (IOException e) {
throw new MailboxException("Unable to search the mailbox", e);
} finally {
if (searcher != null) {
try {
searcher.close();
} catch (IOException e) {
// ignore on close
}
}
}
return results;
}
private Query buildQueryFromMailboxes(ImmutableSet<MailboxId> mailboxIds) {
if (mailboxIds.isEmpty()) {
return new MatchAllDocsQuery();
}
BooleanQuery query = new BooleanQuery();
for (MailboxId id: mailboxIds) {
String idAsString = id.serialize();
query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, idAsString)), BooleanClause.Occur.SHOULD);
}
return query;
}
/**
* Create a new {@link Document} for the given {@link MailboxMessage}. This Document does not contain any flags data. The {@link Flags} are stored in a seperate Document.
*
* See {@link #createFlagsDocument(MailboxMessage)}
*
* @param membership
* @return document
*/
private Document createMessageDocument(final MailboxSession session, final MailboxMessage membership) throws MailboxException{
final Document doc = new Document();
// TODO: Better handling
doc.add(new Field(MAILBOX_ID_FIELD, membership.getMailboxId().serialize().toUpperCase(Locale.ENGLISH), Store.YES, Index.NOT_ANALYZED));
doc.add(new NumericField(UID_FIELD,Store.YES, true).setLongValue(membership.getUid()));
// create an unqiue key for the document which can be used later on updates to find the document
doc.add(new Field(ID_FIELD, membership.getMailboxId().serialize().toUpperCase(Locale.ENGLISH) +"-" + Long.toString(membership.getUid()), Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_YEAR_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.YEAR), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_MONTH_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.MONTH), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_DAY_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.DAY), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_HOUR_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.HOUR), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_MINUTE_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.MINUTE), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_SECOND_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.SECOND), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(INTERNAL_DATE_FIELD_MILLISECOND_RESOLUTION, DateTools.dateToString(membership.getInternalDate(), DateTools.Resolution.MILLISECOND), Store.NO, Index.NOT_ANALYZED));
doc.add(new NumericField(SIZE_FIELD,Store.YES, true).setLongValue(membership.getFullContentOctets()));
// content handler which will index the headers and the body of the message
SimpleContentHandler handler = new SimpleContentHandler() {
public void headers(Header header) {
Date sentDate = null;
String firstFromMailbox = "";
String firstToMailbox = "";
String firstCcMailbox = "";
String firstFromDisplay = "";
String firstToDisplay = "";
Iterator<org.apache.james.mime4j.stream.Field> fields = header.iterator();
while(fields.hasNext()) {
org.apache.james.mime4j.stream.Field f = fields.next();
String headerName = f.getName().toUpperCase(Locale.ENGLISH);
String headerValue = f.getBody().toUpperCase(Locale.ENGLISH);
String fullValue = f.toString().toUpperCase(Locale.ENGLISH);
doc.add(new Field(HEADERS_FIELD, fullValue, Store.NO, Index.ANALYZED));
doc.add(new Field(PREFIX_HEADER_FIELD + headerName, headerValue, Store.NO, Index.ANALYZED));
if (f instanceof DateTimeField) {
// We need to make sure we convert it to GMT
final StringReader reader = new StringReader(f.getBody());
try {
DateTime dateTime = new DateTimeParser(reader).parseAll();
Calendar cal = getGMT();
cal.set(dateTime.getYear(), dateTime.getMonth() - 1, dateTime.getDay(), dateTime.getHour(), dateTime.getMinute(), dateTime.getSecond());
sentDate = cal.getTime();
} catch (org.apache.james.mime4j.field.datetime.parser.ParseException e) {
session.getLog().debug("Unable to parse Date header for proper indexing", e);
// This should never happen anyway fallback to the already parsed field
sentDate = ((DateTimeField) f).getDate();
}
}
String field = null;
if ("To".equalsIgnoreCase(headerName)) {
field = TO_FIELD;
} else if ("From".equalsIgnoreCase(headerName)) {
field = FROM_FIELD;
} else if ("Cc".equalsIgnoreCase(headerName)) {
field = CC_FIELD;
} else if ("Bcc".equalsIgnoreCase(headerName)) {
field = BCC_FIELD;
}
// Check if we can index the the address in the right manner
if (field != null) {
// not sure if we really should reparse it. It maybe be better to check just for the right type.
// But this impl was easier in the first place
AddressList aList = LenientAddressParser.DEFAULT.parseAddressList(MimeUtil.unfold(f.getBody()));
for (int i = 0; i < aList.size(); i++) {
Address address = aList.get(i);
if (address instanceof org.apache.james.mime4j.dom.address.Mailbox) {
org.apache.james.mime4j.dom.address.Mailbox mailbox = (org.apache.james.mime4j.dom.address.Mailbox) address;
String value = AddressFormatter.DEFAULT.encode(mailbox).toUpperCase(Locale.ENGLISH);
doc.add(new Field(field, value, Store.NO, Index.ANALYZED));
if (i == 0) {
String mailboxAddress = SearchUtil.getMailboxAddress(mailbox);
String mailboxDisplay = SearchUtil.getDisplayAddress(mailbox);
if ("To".equalsIgnoreCase(headerName)) {
firstToMailbox = mailboxAddress;
firstToDisplay = mailboxDisplay;
} else if ("From".equalsIgnoreCase(headerName)) {
firstFromMailbox = mailboxAddress;
firstFromDisplay = mailboxDisplay;
} else if ("Cc".equalsIgnoreCase(headerName)) {
firstCcMailbox = mailboxAddress;
}
}
} else if (address instanceof Group) {
MailboxList mList = ((Group) address).getMailboxes();
for (int a = 0; a < mList.size(); a++) {
org.apache.james.mime4j.dom.address.Mailbox mailbox = mList.get(a);
String value = AddressFormatter.DEFAULT.encode(mailbox).toUpperCase(Locale.ENGLISH);
doc.add(new Field(field, value, Store.NO, Index.ANALYZED));
if (i == 0 && a == 0) {
String mailboxAddress = SearchUtil.getMailboxAddress(mailbox);
String mailboxDisplay = SearchUtil.getDisplayAddress(mailbox);
if ("To".equalsIgnoreCase(headerName)) {
firstToMailbox = mailboxAddress;
firstToDisplay = mailboxDisplay;
} else if ("From".equalsIgnoreCase(headerName)) {
firstFromMailbox = mailboxAddress;
firstFromDisplay = mailboxDisplay;
} else if ("Cc".equalsIgnoreCase(headerName)) {
firstCcMailbox = mailboxAddress;
}
}
}
}
}
doc.add(new Field(field, headerValue, Store.NO, Index.ANALYZED));
} else if (headerName.equalsIgnoreCase("Subject")) {
doc.add(new Field(BASE_SUBJECT_FIELD, SearchUtil.getBaseSubject(headerValue), Store.YES, Index.NOT_ANALYZED));
}
}
if (sentDate == null) {
sentDate = membership.getInternalDate();
} else {
doc.add(new Field(SENT_DATE_FIELD_YEAR_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.YEAR), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_MONTH_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.MONTH), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_DAY_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.DAY), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_HOUR_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.HOUR), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_MINUTE_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.MINUTE), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_SECOND_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.SECOND), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(SENT_DATE_FIELD_MILLISECOND_RESOLUTION, DateTools.dateToString(sentDate, DateTools.Resolution.MILLISECOND), Store.NO, Index.NOT_ANALYZED));
}
doc.add(new Field(SENT_DATE_SORT_FIELD_MILLISECOND_RESOLUTION,DateTools.dateToString(sentDate, DateTools.Resolution.MILLISECOND), Store.NO, Index.NOT_ANALYZED));
doc.add(new Field(FIRST_FROM_MAILBOX_NAME_FIELD, firstFromMailbox, Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(FIRST_TO_MAILBOX_NAME_FIELD, firstToMailbox, Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(FIRST_CC_MAILBOX_NAME_FIELD, firstCcMailbox, Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(FIRST_FROM_MAILBOX_DISPLAY_FIELD, firstFromDisplay, Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(FIRST_TO_MAILBOX_DISPLAY_FIELD, firstToDisplay, Store.YES, Index.NOT_ANALYZED));
}
@Override
public void body(BodyDescriptor desc, InputStream in) throws MimeException, IOException {
String mediaType = desc.getMediaType();
if (MEDIA_TYPE_TEXT.equalsIgnoreCase(mediaType) || MEDIA_TYPE_MESSAGE.equalsIgnoreCase(mediaType)) {
String cset = desc.getCharset();
if (cset == null) {
cset = DEFAULT_ENCODING;
}
Charset charset;
try {
charset = Charset.forName(cset);
} catch (Exception e) {
// Invalid charset found so fallback toe the DEFAULT_ENCODING
charset = Charset.forName(DEFAULT_ENCODING);
}
// Read the content one line after the other and add it to the document
BufferedReader bodyReader = new BufferedReader(new InputStreamReader(in, charset));
String line = null;
while((line = bodyReader.readLine()) != null) {
doc.add(new Field(BODY_FIELD, line.toUpperCase(Locale.ENGLISH),Store.NO, Index.ANALYZED));
}
}
}
};
MimeConfig config = MimeConfig.custom()
.setMaxLineLen(-1)
.setMaxContentLen(-1)
.build();
//config.setStrictParsing(false);
MimeStreamParser parser = new MimeStreamParser(config);
parser.setContentDecoding(true);
parser.setContentHandler(handler);
try {
// parse the message to index headers and body
parser.parse(membership.getFullContent());
} catch (MimeException e) {
// This should never happen as it was parsed before too without problems.
throw new MailboxException("Unable to index content of message", e);
} catch (IOException e) {
// This should never happen as it was parsed before too without problems.
// anyway let us just skip the body and headers in the index
throw new MailboxException("Unable to index content of message", e);
}
return doc;
}
private String toSentDateField(DateResolution res) {
String field;
switch (res) {
case Year:
field = SENT_DATE_FIELD_YEAR_RESOLUTION;
break;
case Month:
field = SENT_DATE_FIELD_MONTH_RESOLUTION;
break;
case Day:
field = SENT_DATE_FIELD_DAY_RESOLUTION;
break;
case Hour:
field = SENT_DATE_FIELD_HOUR_RESOLUTION;
break;
case Minute:
field = SENT_DATE_FIELD_MINUTE_RESOLUTION;
break;
case Second:
field = SENT_DATE_FIELD_SECOND_RESOLUTION;
break;
default:
field = SENT_DATE_FIELD_MILLISECOND_RESOLUTION;
break;
}
return field;
}
private static Calendar getGMT() {
return Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH);
}
private String toInteralDateField(DateResolution res) {
String field;
switch (res) {
case Year:
field = INTERNAL_DATE_FIELD_YEAR_RESOLUTION;
break;
case Month:
field = INTERNAL_DATE_FIELD_MONTH_RESOLUTION;
break;
case Day:
field = INTERNAL_DATE_FIELD_DAY_RESOLUTION;
break;
case Hour:
field = INTERNAL_DATE_FIELD_HOUR_RESOLUTION;
break;
case Minute:
field = INTERNAL_DATE_FIELD_MINUTE_RESOLUTION;
break;
case Second:
field = INTERNAL_DATE_FIELD_SECOND_RESOLUTION;
break;
default:
field = INTERNAL_DATE_FIELD_MILLISECOND_RESOLUTION;
break;
}
return field;
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.InternalDateCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createInternalDateQuery(SearchQuery.InternalDateCriterion crit) throws UnsupportedSearchException {
DateOperator dop = crit.getOperator();
DateResolution res = dop.getDateResultion();
String field = toInteralDateField(res);
return createQuery(field, dop);
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.SizeCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createSizeQuery(SearchQuery.SizeCriterion crit) throws UnsupportedSearchException {
NumericOperator op = crit.getOperator();
switch (op.getType()) {
case EQUALS:
return NumericRangeQuery.newLongRange(SIZE_FIELD, op.getValue(), op.getValue(), true, true);
case GREATER_THAN:
return NumericRangeQuery.newLongRange(SIZE_FIELD, op.getValue(), Long.MAX_VALUE, false, true);
case LESS_THAN:
return NumericRangeQuery.newLongRange(SIZE_FIELD, Long.MIN_VALUE, op.getValue(), true, false);
default:
throw new UnsupportedSearchException();
}
}
/**
* This method will return the right {@link Query} depending if {@link #suffixMatch} is enabled
*
* @param fieldName
* @param value
* @return query
*/
private Query createTermQuery(String fieldName, String value) {
if (suffixMatch) {
return new WildcardQuery(new Term(fieldName, "*" + value + "*"));
} else {
return new PrefixQuery(new Term(fieldName, value));
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.HeaderCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createHeaderQuery(SearchQuery.HeaderCriterion crit) throws UnsupportedSearchException {
HeaderOperator op = crit.getOperator();
String name = crit.getHeaderName().toUpperCase(Locale.ENGLISH);
String fieldName = PREFIX_HEADER_FIELD + name;
if (op instanceof SearchQuery.ContainsOperator) {
ContainsOperator cop = (ContainsOperator) op;
return createTermQuery(fieldName, cop.getValue().toUpperCase(Locale.ENGLISH));
} else if (op instanceof SearchQuery.ExistsOperator){
return new PrefixQuery(new Term(fieldName, ""));
} else if (op instanceof SearchQuery.DateOperator) {
DateOperator dop = (DateOperator) op;
String field = toSentDateField(dop.getDateResultion());
return createQuery(field, dop);
} else if (op instanceof SearchQuery.AddressOperator) {
String field = name.toLowerCase(Locale.ENGLISH);
return createTermQuery(field, ((SearchQuery.AddressOperator) op).getAddress().toUpperCase(Locale.ENGLISH));
} else {
// Operator not supported
throw new UnsupportedSearchException();
}
}
private Query createQuery(String field, DateOperator dop) throws UnsupportedSearchException {
Date date = dop.getDate();
DateResolution res = dop.getDateResultion();
DateTools.Resolution dRes = toResolution(res);
String value = DateTools.dateToString(date, dRes);
switch(dop.getType()) {
case ON:
return new TermQuery(new Term(field ,value));
case BEFORE:
return new TermRangeQuery(field, DateTools.dateToString(MIN_DATE, dRes), value, true, false);
case AFTER:
return new TermRangeQuery(field, value, DateTools.dateToString(MAX_DATE, dRes), false, true);
default:
throw new UnsupportedSearchException();
}
}
private DateTools.Resolution toResolution(DateResolution res) {
switch (res) {
case Year:
return DateTools.Resolution.YEAR;
case Month:
return DateTools.Resolution.MONTH;
case Day:
return DateTools.Resolution.DAY;
case Hour:
return DateTools.Resolution.HOUR;
case Minute:
return DateTools.Resolution.MINUTE;
case Second:
return DateTools.Resolution.SECOND;
default:
return DateTools.Resolution.MILLISECOND;
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.UidCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createUidQuery(SearchQuery.UidCriterion crit) throws UnsupportedSearchException {
NumericRange[] ranges = crit.getOperator().getRange();
if (ranges.length == 1) {
NumericRange range = ranges[0];
return NumericRangeQuery.newLongRange(UID_FIELD, range.getLowValue(), range.getHighValue(), true, true);
} else {
BooleanQuery rangesQuery = new BooleanQuery();
for (NumericRange range : ranges) {
rangesQuery.add(NumericRangeQuery.newLongRange(UID_FIELD, range.getLowValue(), range.getHighValue(), true, true), BooleanClause.Occur.SHOULD);
}
return rangesQuery;
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.UidCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createModSeqQuery(SearchQuery.ModSeqCriterion crit) throws UnsupportedSearchException {
NumericOperator op = crit.getOperator();
switch (op.getType()) {
case EQUALS:
return NumericRangeQuery.newLongRange(MODSEQ_FIELD, op.getValue(), op.getValue(), true, true);
case GREATER_THAN:
return NumericRangeQuery.newLongRange(MODSEQ_FIELD, op.getValue(), Long.MAX_VALUE, false, true);
case LESS_THAN:
return NumericRangeQuery.newLongRange(MODSEQ_FIELD, Long.MIN_VALUE, op.getValue(), true, false);
default:
throw new UnsupportedSearchException();
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.FlagCriterion}. This is kind of a hack
* as it will do a search for the flags in this method and
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createFlagQuery(String flag, boolean isSet, Query inMailboxes, Collection<Long> recentUids) throws MailboxException, UnsupportedSearchException {
BooleanQuery query = new BooleanQuery();
if (isSet) {
query.add(new TermQuery(new Term(FLAGS_FIELD, flag)), BooleanClause.Occur.MUST);
} else {
// lucene does not support simple NOT queries so we do some nasty hack here
BooleanQuery bQuery = new BooleanQuery();
bQuery.add(new PrefixQuery(new Term(FLAGS_FIELD, "")), BooleanClause.Occur.MUST);
bQuery.add(new TermQuery(new Term(FLAGS_FIELD, flag)),BooleanClause.Occur.MUST_NOT);
query.add(bQuery, BooleanClause.Occur.MUST);
}
query.add(inMailboxes, BooleanClause.Occur.MUST);
IndexSearcher searcher = null;
try {
Set<Long> uids = new HashSet<Long>();
searcher = new IndexSearcher(IndexReader.open(writer, true));
// query for all the documents sorted by uid
TopDocs docs = searcher.search(query, null, maxQueryResults, new Sort(UID_SORT));
ScoreDoc[] sDocs = docs.scoreDocs;
for (ScoreDoc sDoc : sDocs) {
long uid = Long.valueOf(searcher.doc(sDoc.doc).get(UID_FIELD));
uids.add(uid);
}
// add or remove recent uids
if (flag.equalsIgnoreCase("\\RECENT")){
if (isSet) {
uids.addAll(recentUids);
} else {
uids.removeAll(recentUids);
}
}
List<MessageRange> ranges = MessageRange.toRanges(new ArrayList<Long>(uids));
NumericRange[] nRanges = new NumericRange[ranges.size()];
for (int i = 0; i < ranges.size(); i++) {
MessageRange range = ranges.get(i);
nRanges[i] = new NumericRange(range.getUidFrom(), range.getUidTo());
}
return createUidQuery((UidCriterion) SearchQuery.uid(nRanges));
} catch (IOException e) {
throw new MailboxException("Unable to search mailbox " + inMailboxes, e);
} finally {
if (searcher != null) {
try {
searcher.close();
} catch (IOException e) {
// ignore on close
}
}
}
}
private Sort createSort(List<SearchQuery.Sort> sorts) {
Sort sort = new Sort();
List<SortField> fields = new ArrayList<SortField>();
for (SearchQuery.Sort s : sorts) {
boolean reverse = s.isReverse();
SortField sf = null;
switch (s.getSortClause()) {
case Arrival:
if (reverse) {
sf = ARRIVAL_MAILBOX_SORT_REVERSE;
} else {
sf = ARRIVAL_MAILBOX_SORT;
}
break;
case SentDate:
if (reverse) {
sf = SENT_DATE_SORT_REVERSE;
} else {
sf = SENT_DATE_SORT;
}
break;
case MailboxCc:
if (reverse) {
sf = FIRST_CC_MAILBOX_SORT_REVERSE;
} else {
sf = FIRST_CC_MAILBOX_SORT;
}
break;
case MailboxFrom:
if (reverse) {
sf = FIRST_FROM_MAILBOX_SORT_REVERSE;
} else {
sf = FIRST_FROM_MAILBOX_SORT;
}
break;
case Size:
if (reverse) {
sf = SIZE_SORT_REVERSE;
} else {
sf = SIZE_SORT;
}
break;
case BaseSubject:
if (reverse) {
sf = BASE_SUBJECT_SORT_REVERSE;
} else {
sf = BASE_SUBJECT_SORT;
}
break;
case MailboxTo:
if (reverse) {
sf = FIRST_TO_MAILBOX_SORT_REVERSE;
} else {
sf = FIRST_TO_MAILBOX_SORT;
}
break;
case Uid:
if (reverse) {
sf = UID_SORT_REVERSE;
} else {
sf = UID_SORT;
}
break;
case DisplayFrom:
if (reverse) {
sf = FIRST_FROM_MAILBOX_DISPLAY_SORT_REVERSE;
} else {
sf = FIRST_FROM_MAILBOX_DISPLAY_SORT;
}
break;
case DisplayTo:
if (reverse) {
sf = FIRST_TO_MAILBOX_DISPLAY_SORT_REVERSE;
} else {
sf = FIRST_TO_MAILBOX_DISPLAY_SORT;
}
break;
default:
break;
}
if (sf != null) {
fields.add(sf);
// Add the uid sort as tie-breaker
if (sf == SENT_DATE_SORT) {
fields.add(UID_SORT);
} else if (sf == SENT_DATE_SORT_REVERSE) {
fields.add(UID_SORT_REVERSE);
}
}
}
// add the uid sorting as last so if no other sorting was able todo the job it will get sorted by the uid
fields.add(UID_SORT);
sort.setSort(fields.toArray(new SortField[0]));
return sort;
}
/**
* Convert the given {@link Flag} to a String
*
* @param flag
* @return flagString
*/
private String toString(Flag flag) {
if (Flag.ANSWERED.equals(flag)) {
return "\\ANSWERED";
} else if (Flag.DELETED.equals(flag)) {
return "\\DELETED";
} else if (Flag.DRAFT.equals(flag)) {
return "\\DRAFT";
} else if (Flag.FLAGGED.equals(flag)) {
return "\\FLAGGED";
} else if (Flag.RECENT.equals(flag)) {
return "\\RECENT";
} else if (Flag.SEEN.equals(flag)) {
return "\\FLAG";
} else {
return flag.toString();
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.TextCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createTextQuery(SearchQuery.TextCriterion crit) throws UnsupportedSearchException {
String value = crit.getOperator().getValue().toUpperCase(Locale.ENGLISH);
switch(crit.getType()) {
case BODY:
return createTermQuery(BODY_FIELD, value);
case FULL:
BooleanQuery query = new BooleanQuery();
query.add(createTermQuery(BODY_FIELD, value), BooleanClause.Occur.SHOULD);
query.add(createTermQuery(HEADERS_FIELD,value), BooleanClause.Occur.SHOULD);
return query;
default:
throw new UnsupportedSearchException();
}
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.AllCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createAllQuery(SearchQuery.AllCriterion crit) throws UnsupportedSearchException{
BooleanQuery query = new BooleanQuery();
query.add(createQuery(MessageRange.all()), BooleanClause.Occur.MUST);
query.add(new PrefixQuery(new Term(FLAGS_FIELD, "")), BooleanClause.Occur.MUST_NOT);
return query;
}
/**
* Return a {@link Query} which is build based on the given {@link SearchQuery.ConjunctionCriterion}
*
* @param crit
* @return query
* @throws UnsupportedSearchException
*/
private Query createConjunctionQuery(SearchQuery.ConjunctionCriterion crit, Query inMailboxes, Collection<Long> recentUids) throws UnsupportedSearchException, MailboxException {
List<Criterion> crits = crit.getCriteria();
BooleanQuery conQuery = new BooleanQuery();
switch (crit.getType()) {
case AND:
for (Criterion criterion : crits) {
conQuery.add(createQuery(criterion, inMailboxes, recentUids), BooleanClause.Occur.MUST);
}
return conQuery;
case OR:
for (Criterion criterion : crits) {
conQuery.add(createQuery(criterion, inMailboxes, recentUids), BooleanClause.Occur.SHOULD);
}
return conQuery;
case NOR:
BooleanQuery nor = new BooleanQuery();
for (Criterion criterion : crits) {
conQuery.add(createQuery(criterion, inMailboxes, recentUids), BooleanClause.Occur.SHOULD);
}
nor.add(inMailboxes, BooleanClause.Occur.MUST);
nor.add(conQuery, BooleanClause.Occur.MUST_NOT);
return nor;
default:
throw new UnsupportedSearchException();
}
}
/**
* Return a {@link Query} which is builded based on the given {@link Criterion}
*
* @param criterion
* @return query
* @throws UnsupportedSearchException
*/
private Query createQuery(Criterion criterion, Query inMailboxes, Collection<Long> recentUids) throws UnsupportedSearchException, MailboxException {
if (criterion instanceof SearchQuery.InternalDateCriterion) {
SearchQuery.InternalDateCriterion crit = (SearchQuery.InternalDateCriterion) criterion;
return createInternalDateQuery(crit);
} else if (criterion instanceof SearchQuery.SizeCriterion) {
SearchQuery.SizeCriterion crit = (SearchQuery.SizeCriterion) criterion;
return createSizeQuery(crit);
} else if (criterion instanceof SearchQuery.HeaderCriterion) {
HeaderCriterion crit = (HeaderCriterion) criterion;
return createHeaderQuery(crit);
} else if (criterion instanceof SearchQuery.UidCriterion) {
SearchQuery.UidCriterion crit = (SearchQuery.UidCriterion) criterion;
return createUidQuery(crit);
} else if (criterion instanceof SearchQuery.FlagCriterion) {
FlagCriterion crit = (FlagCriterion) criterion;
return createFlagQuery(toString(crit.getFlag()), crit.getOperator().isSet(), inMailboxes, recentUids);
} else if (criterion instanceof SearchQuery.CustomFlagCriterion) {
CustomFlagCriterion crit = (CustomFlagCriterion) criterion;
return createFlagQuery(crit.getFlag(), crit.getOperator().isSet(), inMailboxes, recentUids);
} else if (criterion instanceof SearchQuery.TextCriterion) {
SearchQuery.TextCriterion crit = (SearchQuery.TextCriterion) criterion;
return createTextQuery(crit);
} else if (criterion instanceof SearchQuery.AllCriterion) {
return createAllQuery((AllCriterion) criterion);
} else if (criterion instanceof SearchQuery.ConjunctionCriterion) {
SearchQuery.ConjunctionCriterion crit = (SearchQuery.ConjunctionCriterion) criterion;
return createConjunctionQuery(crit, inMailboxes, recentUids);
} else if (criterion instanceof SearchQuery.ModSeqCriterion) {
return createModSeqQuery((SearchQuery.ModSeqCriterion) criterion);
}
throw new UnsupportedSearchException();
}
/**
* @see org.apache.james.mailbox.store.search.ListeningMessageSearchIndex#add(org.apache.james.mailbox.MailboxSession, org.apache.james.mailbox.store.mail.model.Mailbox, MailboxMessage)
*/
public void add(MailboxSession session, Mailbox mailbox, MailboxMessage membership) throws MailboxException {
Document doc = createMessageDocument(session, membership);
Document flagsDoc = createFlagsDocument(membership);
try {
writer.addDocument(doc);
writer.addDocument(flagsDoc);
} catch (CorruptIndexException e) {
throw new MailboxException("Unable to add message to index", e);
} catch (IOException e) {
throw new MailboxException("Unable to add message to index", e);
}
}
/**
* @see ListeningMessageSearchIndex#update
*/
@Override
public void update(MailboxSession session, Mailbox mailbox, List<UpdatedFlags> updatedFlagsList) throws MailboxException {
for (UpdatedFlags updatedFlags : updatedFlagsList) {
update(mailbox, updatedFlags.getUid(), updatedFlags.getNewFlags());
}
}
private void update(Mailbox mailbox, long uid, Flags f) throws MailboxException {
IndexSearcher searcher = null;
try {
searcher = new IndexSearcher(IndexReader.open(writer, true));
BooleanQuery query = new BooleanQuery();
query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, mailbox.getMailboxId().serialize())), BooleanClause.Occur.MUST);
query.add(createQuery(MessageRange.one(uid)), BooleanClause.Occur.MUST);
query.add( new PrefixQuery(new Term(FLAGS_FIELD, "")), BooleanClause.Occur.MUST);
TopDocs docs = searcher.search(query, 100000);
ScoreDoc[] sDocs = docs.scoreDocs;
for (ScoreDoc sDoc : sDocs) {
Document doc = searcher.doc(sDoc.doc);
if (doc.getFieldable(FLAGS_FIELD) == null) {
doc.removeFields(FLAGS_FIELD);
indexFlags(doc, f);
writer.updateDocument(new Term(ID_FIELD, doc.get(ID_FIELD)), doc);
}
}
} catch (IOException e) {
throw new MailboxException("Unable to add messages in index", e);
} finally {
try {
IOUtils.closeWhileHandlingException(searcher);
} catch (IOException e) {
//can't happen anyway
}
}
}
/**
* Index the {@link Flags} and add it to the {@link Document}
*
* @param f
* @param doc
*/
private Document createFlagsDocument(MailboxMessage message) {
Document doc = new Document();
doc.add(new Field(ID_FIELD, "flags-" + message.getMailboxId().serialize() +"-" + Long.toString(message.getUid()), Store.YES, Index.NOT_ANALYZED));
doc.add(new Field(MAILBOX_ID_FIELD, message.getMailboxId().serialize(), Store.YES, Index.NOT_ANALYZED));
doc.add(new NumericField(UID_FIELD,Store.YES, true).setLongValue(message.getUid()));
indexFlags(doc, message.createFlags());
return doc;
}
/**
* Add the given {@link Flags} to the {@link Document}
*
* @param doc
* @param f
*/
private void indexFlags(Document doc, Flags f) {
List<String> fString = new ArrayList<String>();
Flag[] flags = f.getSystemFlags();
for (Flag flag : flags) {
fString.add(toString(flag));
doc.add(new Field(FLAGS_FIELD, toString(flag), Store.NO, Index.NOT_ANALYZED));
}
String[] userFlags = f.getUserFlags();
for (String userFlag : userFlags) {
doc.add(new Field(FLAGS_FIELD, userFlag, Store.NO, Index.NOT_ANALYZED));
}
// if no flags are there we just use a empty field
if (flags.length == 0 && userFlags.length == 0) {
doc.add(new Field(FLAGS_FIELD, "",Store.NO, Index.NOT_ANALYZED));
}
}
private Query createQuery(MessageRange range) {
switch (range.getType()) {
case ONE:
return NumericRangeQuery.newLongRange(UID_FIELD, range.getUidFrom(), range.getUidTo(), true, true);
case FROM:
return NumericRangeQuery.newLongRange(UID_FIELD, range.getUidFrom(), Long.MAX_VALUE, true, true);
default:
return NumericRangeQuery.newLongRange(UID_FIELD, 0L, Long.MAX_VALUE, true, true);
}
}
@Override
public void delete(MailboxSession session, Mailbox mailbox, List<Long> expungedUids) throws MailboxException {
Collection<MessageRange> messageRanges = MessageRange.toRanges(expungedUids);
for (MessageRange messageRange : messageRanges) {
delete(mailbox, messageRange);
}
}
@Override
public void deleteAll(MailboxSession session, Mailbox mailbox) throws MailboxException {
delete(mailbox, MessageRange.all());
}
public void delete(Mailbox mailbox, MessageRange range) throws MailboxException {
BooleanQuery query = new BooleanQuery();
query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, mailbox.getMailboxId().serialize())), BooleanClause.Occur.MUST);
query.add(createQuery(range), BooleanClause.Occur.MUST);
try {
writer.deleteDocuments(query);
} catch (CorruptIndexException e) {
throw new MailboxException("Unable to delete message from index", e);
} catch (IOException e) {
throw new MailboxException("Unable to delete message from index", e);
}
}
}