Author: norman
Date: Tue Feb 15 15:18:54 2011
New Revision: 1070933

URL: http://svn.apache.org/viewvc?rev=1070933&view=rev
Log:
Add a new interface called MessageSearchIndex and an implementation which use 
lucene to index messages and make it possible to search within a mailbox in a 
performant way. This is just the first part of it as we need to think about 
howto invoke the MessageSearchIndex. See MAILBOX-10

Added:
    
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/MessageSearchIndex.java
    
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/
    
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/ImapSearchAnalyzer.java
    
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndex.java
    
james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/
    
james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndexTest.java
Modified:
    james/mailbox/trunk/parent/pom.xml
    james/mailbox/trunk/store/pom.xml

Modified: james/mailbox/trunk/parent/pom.xml
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/parent/pom.xml?rev=1070933&r1=1070932&r2=1070933&view=diff
==============================================================================
--- james/mailbox/trunk/parent/pom.xml (original)
+++ james/mailbox/trunk/parent/pom.xml Tue Feb 15 15:18:54 2011
@@ -242,6 +242,17 @@
       <!--
         END Modules
       -->
+      <dependency>
+        <groupId>org.apache.lucene</groupId>
+        <artifactId>lucene-core</artifactId>
+        <version>3.0.3</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.lucene</groupId>
+        <artifactId>lucene-analyzers</artifactId>
+        <version>3.0.3</version>
+      </dependency>
+
 
       <!--
         START Mail

Modified: james/mailbox/trunk/store/pom.xml
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/store/pom.xml?rev=1070933&r1=1070932&r2=1070933&view=diff
==============================================================================
--- james/mailbox/trunk/store/pom.xml (original)
+++ james/mailbox/trunk/store/pom.xml Tue Feb 15 15:18:54 2011
@@ -46,6 +46,14 @@
       <artifactId>${javax.mail.artifactId}</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.apache.lucene</groupId>
+      <artifactId>lucene-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.lucene</groupId>
+      <artifactId>lucene-analyzers</artifactId>
+    </dependency>
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>test</scope>

Added: 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/MessageSearchIndex.java
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/MessageSearchIndex.java?rev=1070933&view=auto
==============================================================================
--- 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/MessageSearchIndex.java
 (added)
+++ 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/MessageSearchIndex.java
 Tue Feb 15 15:18:54 2011
@@ -0,0 +1,53 @@
+package org.apache.james.mailbox.store;
+
+import java.util.Iterator;
+
+import javax.mail.Flags;
+
+import org.apache.james.mailbox.MailboxException;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageRange;
+import org.apache.james.mailbox.SearchQuery;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.james.mailbox.store.mail.model.MailboxMembership;
+
+public interface MessageSearchIndex<Id> {
+
+    /**
+     * Add the {@link MailboxMembership} to the search index
+     * 
+     * @param mailbox
+     * @param membership
+     * @throws MailboxException
+     */
+    public void add(MailboxSession session, Mailbox<Id> mailbox, 
MailboxMembership<Id> membership) throws MailboxException;
+    
+    /**
+     * Update the Flags in the search index for the given {@link MessageRange} 
+     * 
+     * @param mailbox
+     * @param range
+     * @param flags
+     * @throws MailboxException
+     */
+    public void update(MailboxSession session, Mailbox<Id> mailbox, 
MessageRange range, Flags flags) throws MailboxException;
+    
+    /**
+     * Delete the data for the given {@link MessageRange} from the search index
+     * 
+     * @param mailbox
+     * @param range
+     * @throws MailboxException
+     */
+    public void delete(MailboxSession session, Mailbox<Id> mailbox, 
MessageRange range) throws MailboxException;
+
+    /**
+     * Return all uids of the previous indexed {@link MailboxMembership}'s 
which match the {@link SearchQuery}
+     * 
+     * @param mailbox
+     * @param searchQuery
+     * @return
+     * @throws MailboxException
+     */
+    public Iterator<Long> search(MailboxSession session, Mailbox<Id> mailbox, 
SearchQuery searchQuery) throws MailboxException;
+}

Added: 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/ImapSearchAnalyzer.java
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/ImapSearchAnalyzer.java?rev=1070933&view=auto
==============================================================================
--- 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/ImapSearchAnalyzer.java
 (added)
+++ 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/ImapSearchAnalyzer.java
 Tue Feb 15 15:18:54 2011
@@ -0,0 +1,49 @@
+/****************************************************************
+ * 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.lucene;
+
+import java.io.Reader;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.WhitespaceTokenizer;
+import org.apache.lucene.analysis.ngram.NGramTokenFilter;
+
+/**
+*
+* {@link Analyzer} which match substrings. This is needed because of RFC 3501.
+* 
+* From RFC:
+* 
+*      In all search keys that use strings, a message matches the key if
+*      the string is a substring of the field.  The matching is
+*      case-insensitive.
+*
+*/
+public final class ImapSearchAnalyzer extends Analyzer {
+
+   /*
+    * (non-Javadoc)
+    * @see org.apache.lucene.analysis.Analyzer#tokenStream(java.lang.String, 
java.io.Reader)
+    */
+   public TokenStream tokenStream(String fieldName, Reader reader) {
+       return new NGramTokenFilter(new WhitespaceTokenizer(reader),2 , 4);
+   }
+   
+}
\ No newline at end of file

Added: 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndex.java
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndex.java?rev=1070933&view=auto
==============================================================================
--- 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndex.java
 (added)
+++ 
james/mailbox/trunk/store/src/main/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndex.java
 Tue Feb 15 15:18:54 2011
@@ -0,0 +1,587 @@
+/****************************************************************
+ * 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.lucene;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import javax.mail.Flags;
+import javax.mail.Flags.Flag;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mailbox.MailboxException;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageRange;
+import org.apache.james.mailbox.SearchQuery;
+import org.apache.james.mailbox.SearchQuery.AllCriterion;
+import org.apache.james.mailbox.SearchQuery.ContainsOperator;
+import org.apache.james.mailbox.SearchQuery.Criterion;
+import org.apache.james.mailbox.SearchQuery.DateOperator;
+import org.apache.james.mailbox.SearchQuery.FlagCriterion;
+import org.apache.james.mailbox.SearchQuery.HeaderCriterion;
+import org.apache.james.mailbox.SearchQuery.HeaderOperator;
+import org.apache.james.mailbox.SearchQuery.NumericOperator;
+import org.apache.james.mailbox.SearchQuery.NumericRange;
+import org.apache.james.mailbox.UnsupportedSearchException;
+import org.apache.james.mailbox.store.MessageSearchIndex;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.james.mailbox.store.mail.model.MailboxMembership;
+import org.apache.james.mime4j.MimeException;
+import org.apache.james.mime4j.descriptor.BodyDescriptor;
+import org.apache.james.mime4j.message.Header;
+import org.apache.james.mime4j.message.SimpleContentHandler;
+import org.apache.james.mime4j.parser.MimeStreamParser;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.PerFieldAnalyzerWrapper;
+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.IndexWriter;
+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.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.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.LockObtainFailedException;
+
+/**
+ * Lucene based {@link MessageSearchIndex} which offers message searching.
+ * 
+
+ * @param <Id>
+ */
+public class LuceneMessageSearchIndex<Id> implements MessageSearchIndex<Id>{
+
+    /**
+     * 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 MailboxMembership}
+     */
+    public final static String UID_FIELD = "uid";
+    
+    /**
+     * {@link Field} which will contain the {@link Flags} of the {@link 
MailboxMembership}
+     */
+    public final static String FLAGS_FIELD = "flags";
+  
+    /**
+     * {@link Field} which will contain the size of the {@link 
MailboxMembership}
+     */
+    public final static String SIZE_FIELD = "size";
+
+    /**
+     * {@link Field} which will contain the body of the {@link 
MailboxMembership}
+     */
+    public final static String BODY_FIELD = "body";
+    
+    
+    public final static String PREFIX_HEADER_FIELD ="header_";
+    
+    /**
+     * {@link Field} which will contain the whole message header of the {@link 
MailboxMembership}
+     */
+    public final static String HEADERS_FIELD ="headers";
+    
+    /**
+     * {@link Field} which will contain the internalDate of the {@link 
MailboxMembership}
+     */
+    public final static String INTERNAL_DATE_FIELD ="internaldate";
+ 
+    /**
+     * {@link Field} which will contain the id of the {@link Mailbox}
+     */
+    public final static String MAILBOX_ID_FIELD ="mailboxid";
+
+    
+    private final static String MEDIA_TYPE_TEXT = "text"; 
+    private final static String MEDIA_TYPE_MESSAGE = "message"; 
+
+    private final IndexWriter writer;
+    
+    private int maxQueryResults = DEFAULT_MAX_QUERY_RESULTS;
+    
+    private final static Sort UID_SORT = new Sort(new SortField(UID_FIELD, 
SortField.LONG));
+    
+    public LuceneMessageSearchIndex(Directory directory) throws 
CorruptIndexException, LockObtainFailedException, IOException {
+        this(new IndexWriter(directory, createAnalyzer(), true, 
IndexWriter.MaxFieldLength.UNLIMITED));
+    }
+    
+   
+    public LuceneMessageSearchIndex(IndexWriter writer) {
+        this.writer = writer;
+    }
+    
+    /**
+     * Set the max count of results which will get returned from a query
+     * 
+     * @param maxQueryResults
+     */
+    public void setMaxQueryResults(int maxQueryResults) {
+        this.maxQueryResults = maxQueryResults;
+    }
+    /**
+     * Create a {@link Analyzer} which is used to index the {@link 
MailboxMembership}'s
+     * 
+     * @return analyzer
+     */
+    public static Analyzer createAnalyzer() {
+        PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(new 
ImapSearchAnalyzer());
+        return wrapper;
+    }
+    
+    /*
+     * (non-Javadoc)
+     * @see 
org.apache.james.mailbox.store.MessageSearchIndex#search(org.apache.james.mailbox.MailboxSession,
 org.apache.james.mailbox.store.mail.model.Mailbox, 
org.apache.james.mailbox.SearchQuery)
+     */
+    public Iterator<Long> search(MailboxSession session, Mailbox<Id> mailbox, 
SearchQuery searchQuery) throws MailboxException {
+        List<Long> uids = new ArrayList<Long>();
+        IndexSearcher searcher = null;
+
+        try {
+            searcher = new IndexSearcher(writer.getReader());
+            BooleanQuery query = new BooleanQuery();
+            query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, 
mailbox.getMailboxId().toString())), BooleanClause.Occur.MUST);
+            query.add(createQuery(searchQuery), BooleanClause.Occur.MUST);
+            
+            // query for all the documents sorted by uid
+            TopDocs docs = searcher.search(query, null, maxQueryResults, 
UID_SORT);
+            ScoreDoc[] sDocs = docs.scoreDocs;
+            for (int i = 0; i < sDocs.length; i++) {
+                long uid = 
Long.valueOf(searcher.doc(sDocs[i].doc).get(UID_FIELD));
+                uids.add(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 uids.iterator();
+    }
+
+    /**
+     * Create a new {@link Document} for the given {@link MailboxMembership}
+     * 
+     * @param membership
+     * @return document
+     */
+    public static Document createDocument(MailboxMembership<?> membership) 
throws MailboxException{
+        final Document doc = new Document();
+        // TODO: Better handling
+        doc.add(new Field(MAILBOX_ID_FIELD, 
membership.getMailboxId().toString(), Store.NO, 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().toString() +"-" 
+ Long.toString(membership.getUid()), Store.NO, Index.NOT_ANALYZED));
+        
+        // add flags
+        indexFlags(membership.createFlags(), doc);
+
+        // store the internal date with resulution of a DAY
+        doc.add(new NumericField(INTERNAL_DATE_FIELD,Store.NO, 
true).setLongValue(DateTools.round(membership.getInternalDate().getTime(),DateTools.Resolution.DAY)));
+        doc.add(new NumericField(SIZE_FIELD,Store.NO, 
true).setLongValue(membership.getMessage().getFullContentOctets()));
+        
+        // content handler which will index the headers and the body of the 
message
+        SimpleContentHandler handler = new SimpleContentHandler() {
+            
+            
+            /**
+             * Add the headers to the Document
+             */
+            public void headers(Header header) {
+                
+                Iterator<org.apache.james.mime4j.parser.Field> fields = 
header.iterator();
+                while(fields.hasNext()) {
+                   org.apache.james.mime4j.parser.Field f = fields.next();
+                    doc.add(new Field(HEADERS_FIELD, f.toString() ,Store.NO, 
Index.ANALYZED));
+                    doc.add(new Field(PREFIX_HEADER_FIELD + f.getName(), 
f.getBody() ,Store.NO, Index.ANALYZED));
+                }
+           
+            }
+            
+
+
+            /**
+             * Add the body parts to the Document
+             */
+            public void bodyDecoded(BodyDescriptor desc, InputStream in) 
throws IOException {
+                String mediaType = desc.getMediaType();
+                String charset = desc.getCharset();
+                if (MEDIA_TYPE_TEXT.equalsIgnoreCase(mediaType) || 
MEDIA_TYPE_MESSAGE.equalsIgnoreCase(mediaType)) {
+                    // TODO: maybe we want to limit the length here ?
+                    ByteArrayOutputStream out = new ByteArrayOutputStream();
+                    IOUtils.copy(in, out);
+                    doc.add(new Field(BODY_FIELD,  
out.toString(charset),Store.NO, Index.ANALYZED));
+                    
+                }
+            }
+        };
+        
+        MimeStreamParser parser = new MimeStreamParser();
+        parser.setContentHandler(handler);
+        try {
+            // paese the message to index headers and body
+            parser.parse(membership.getMessage().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;
+    }
+    /**
+     * Create a {@link Query} based on the given {@link SearchQuery}
+     * 
+     * @param searchQuery
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createQuery(SearchQuery searchQuery) throws 
UnsupportedSearchException {
+        List<Criterion> crits = searchQuery.getCriterias();
+        BooleanQuery booleanQuery = new BooleanQuery();
+
+        for (int i = 0; i < crits.size(); i++) {
+            booleanQuery.add(createQuery(crits.get(i)), 
BooleanClause.Occur.MUST);
+        }
+        return booleanQuery;
+
+    }
+
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.InternalDateCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query 
createInternalDateQuery(SearchQuery.InternalDateCriterion crit) throws 
UnsupportedSearchException {
+        DateOperator op = crit.getOperator();
+        Calendar cal = Calendar.getInstance();
+        cal.set(op.getYear(), op.getMonth() - 1, op.getDay());         
+        long value = DateTools.round(cal.getTimeInMillis(), 
DateTools.Resolution.DAY);
+        
+        switch(op.getType()) {
+        case ON:
+            return NumericRangeQuery.newLongRange(INTERNAL_DATE_FIELD ,value, 
value, true, true);
+        case BEFORE: 
+            return NumericRangeQuery.newLongRange(INTERNAL_DATE_FIELD ,0L, 
value, true, false);
+        case AFTER: 
+            return NumericRangeQuery.newLongRange(INTERNAL_DATE_FIELD ,value, 
Long.MAX_VALUE, false, true);
+        default:
+            throw new UnsupportedSearchException();
+        }
+    }
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.SizeCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static 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();
+        }
+    }
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.HeaderCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createHeaderQuery(SearchQuery.HeaderCriterion crit) 
throws UnsupportedSearchException {
+        HeaderOperator op = crit.getOperator();
+        String fieldName = PREFIX_HEADER_FIELD + crit.getHeaderName();
+        if (op instanceof SearchQuery.ContainsOperator) {
+            ContainsOperator cop = (ContainsOperator) op;
+            return new TermQuery(new Term(fieldName, cop.getValue()));
+        } else if (op instanceof SearchQuery.ExistsOperator){
+            return new PrefixQuery(new Term(fieldName, ""));
+        } else {
+            // Operator not supported
+            throw new UnsupportedSearchException();
+        }
+    }
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.UidCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createUidQuery(SearchQuery.UidCriterion crit) throws 
UnsupportedSearchException {
+        NumericRange[] ranges = crit.getOperator().getRange();
+        BooleanQuery rangesQuery = new BooleanQuery();
+        for (int i = 0; i < ranges.length; i++) {
+            NumericRange range = ranges[i];
+            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.FlagCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createFlagQuery(SearchQuery.FlagCriterion crit) throws 
UnsupportedSearchException {
+        Flag flag = crit.getFlag();
+        String value = flag.toString();
+        TermQuery query = new TermQuery(new Term(FLAGS_FIELD, value));
+        if (crit.getOperator().isSet()) {   
+            return query;
+        } 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(UID_FIELD, "")), 
BooleanClause.Occur.MUST);
+            bQuery.add(query, BooleanClause.Occur.MUST_NOT);
+            return bQuery;
+        }
+    }
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.TextCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createTextQuery(SearchQuery.TextCriterion crit) throws 
UnsupportedSearchException {
+        switch(crit.getType()) {
+        case BODY:
+            return new TermQuery(new Term(BODY_FIELD, 
crit.getOperator().getValue().toLowerCase(Locale.US)));
+        case FULL: 
+            BooleanQuery query = new BooleanQuery();
+            query.add(new TermQuery(new Term(BODY_FIELD, 
crit.getOperator().getValue().toLowerCase(Locale.US))), 
BooleanClause.Occur.SHOULD);
+            query.add(new TermQuery(new Term(HEADERS_FIELD, 
crit.getOperator().getValue().toLowerCase(Locale.US))), 
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
+     */
+    public static Query createAllQuery(SearchQuery.AllCriterion crit) throws 
UnsupportedSearchException{
+        return NumericRangeQuery.newLongRange(UID_FIELD, Long.MIN_VALUE, 
Long.MAX_VALUE, true, true);
+    }
+    
+    /**
+     * Return a {@link Query} which is build based on the given {@link 
SearchQuery.ConjunctionCriterion}
+     * 
+     * @param crit
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query 
createConjunctionQuery(SearchQuery.ConjunctionCriterion crit) throws 
UnsupportedSearchException {
+        BooleanClause.Occur occur;
+        switch (crit.getType()) {
+        case AND:
+            occur = BooleanClause.Occur.MUST;
+            break;
+        case OR:
+            occur = BooleanClause.Occur.SHOULD;
+            break;
+        case NOR:
+            occur = BooleanClause.Occur.MUST_NOT;
+            break;
+        default:
+            throw new UnsupportedSearchException();
+        }
+        List<Criterion> crits = crit.getCriteria();
+        BooleanQuery conQuery = new BooleanQuery();
+        for (int i = 0; i < crits.size(); i++) {
+            conQuery.add(createQuery(crits.get(i)), occur);
+        }
+        return conQuery;
+    }
+    
+    /**
+     * Return a {@link Query} which is builded based on the given {@link 
Criterion}
+     * 
+     * @param criterion
+     * @return query
+     * @throws UnsupportedSearchException
+     */
+    public static Query createQuery(Criterion criterion) throws 
UnsupportedSearchException {
+        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(crit);
+        } 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);
+        }
+        throw new UnsupportedSearchException();
+
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see 
org.apache.james.mailbox.store.MessageSearchIndex#add(org.apache.james.mailbox.MailboxSession,
 org.apache.james.mailbox.store.mail.model.Mailbox, 
org.apache.james.mailbox.store.mail.model.MailboxMembership)
+     */
+    public void add(MailboxSession session, Mailbox<Id> mailbox, 
MailboxMembership<Id> membership) throws MailboxException {
+        Document doc = createDocument(membership);
+        try {
+            writer.addDocument(doc);
+        } 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);
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see 
org.apache.james.mailbox.store.MessageSearchIndex#update(org.apache.james.mailbox.MailboxSession,
 org.apache.james.mailbox.store.mail.model.Mailbox, 
org.apache.james.mailbox.MessageRange, javax.mail.Flags)
+     */
+    public void update(MailboxSession session, Mailbox<Id> mailbox, 
MessageRange range, Flags f) throws MailboxException {
+        try {
+            IndexSearcher searcher = new IndexSearcher(writer.getReader());
+            BooleanQuery query = new BooleanQuery();
+            query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, 
mailbox.getMailboxId().toString())), BooleanClause.Occur.MUST);
+            query.add(NumericRangeQuery.newLongRange(UID_FIELD, 
range.getUidFrom(), range.getUidTo(), true, true), BooleanClause.Occur.MUST);
+            TopDocs docs = searcher.search(query, 100000);
+            ScoreDoc[] sDocs = docs.scoreDocs;
+            for (int i = 0; i < sDocs.length; i++) {
+                Document doc = searcher.doc(sDocs[i].doc);
+                doc.removeFields(FLAGS_FIELD);
+                indexFlags(f, doc);
+                writer.updateDocument(new Term(ID_FIELD, doc.get(ID_FIELD)), 
doc);
+            }
+        } catch (IOException e) {
+            throw new MailboxException("Unable to add messages in index", e);
+
+        }
+        
+    }
+
+    /**
+     * Index the {@link Flags} and add it to the {@link Document}
+     * 
+     * @param f
+     * @param doc
+     */
+    private static void indexFlags(Flags f, Document doc) {
+        Flag[] flags = f.getSystemFlags();
+        for (int a = 0; a < flags.length; a++) {
+            doc.add(new Field(FLAGS_FIELD, flags[a].toString(),Store.NO, 
Index.NOT_ANALYZED));
+        }
+        
+        String[] userFlags = f.getUserFlags();
+        for (int a = 0; a < userFlags.length; a++) {
+            doc.add(new Field(FLAGS_FIELD, userFlags[a],Store.NO, 
Index.NOT_ANALYZED));
+        }
+    }
+    /*
+     * (non-Javadoc)
+     * @see 
org.apache.james.mailbox.store.MessageSearchIndex#delete(org.apache.james.mailbox.MailboxSession,
 org.apache.james.mailbox.store.mail.model.Mailbox, 
org.apache.james.mailbox.MessageRange)
+     */
+    public void delete(MailboxSession session, Mailbox<Id> mailbox, 
MessageRange range) throws MailboxException {
+        BooleanQuery query = new BooleanQuery();
+        query.add(new TermQuery(new Term(MAILBOX_ID_FIELD, 
mailbox.getMailboxId().toString())), BooleanClause.Occur.MUST);
+        query.add(NumericRangeQuery.newLongRange(UID_FIELD, 
range.getUidFrom(), range.getUidTo(), true, true), 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);
+        }
+    }
+    
+
+
+}

Added: 
james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndexTest.java
URL: 
http://svn.apache.org/viewvc/james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndexTest.java?rev=1070933&view=auto
==============================================================================
--- 
james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndexTest.java
 (added)
+++ 
james/mailbox/trunk/store/src/test/java/org/apache/james/mailbox/store/lucene/LuceneMessageSearchIndexTest.java
 Tue Feb 15 15:18:54 2011
@@ -0,0 +1,297 @@
+/****************************************************************
+ * 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.lucene;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.mail.Flags;
+import javax.mail.Flags.Flag;
+
+import org.apache.james.mailbox.SearchQuery;
+import org.apache.james.mailbox.mock.MockMailboxSession;
+import org.apache.james.mailbox.store.MessageSearchIndex;
+import org.apache.james.mailbox.store.SimpleHeader;
+import org.apache.james.mailbox.store.SimpleMailboxMembership;
+import org.apache.james.mailbox.store.mail.model.Mailbox;
+import org.apache.lucene.store.RAMDirectory;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LuceneMessageSearchIndexTest {
+
+    private MessageSearchIndex<Long> index;
+    private MockMailboxSession session = new MockMailboxSession("myuser");
+    private SimpleMailbox mailbox = new SimpleMailbox(0);
+    private SimpleMailbox mailbox2 = new SimpleMailbox(1);
+
+
+    @Before
+    public void setUp() throws Exception {
+        index = new LuceneMessageSearchIndex<Long>(new RAMDirectory());
+        List<org.apache.james.mailbox.store.SimpleHeader> headersSubject = new 
ArrayList<org.apache.james.mailbox.store.SimpleHeader>();
+        headersSubject.add(new SimpleHeader("Subject", 1, "test"));
+       
+        List<org.apache.james.mailbox.store.SimpleHeader> headersTest = new 
ArrayList<org.apache.james.mailbox.store.SimpleHeader>();
+        headersSubject.add(new SimpleHeader("Test", 1, "test"));
+        
+        List<org.apache.james.mailbox.store.SimpleHeader> headersTestSubject = 
new ArrayList<org.apache.james.mailbox.store.SimpleHeader>();
+        headersTestSubject.add(new SimpleHeader("Test", 1, "test"));
+        headersTestSubject.add(new SimpleHeader("Subject", 2, "test2"));
+
+        SimpleMailboxMembership m = new 
SimpleMailboxMembership(mailbox.getMailboxId(),1, new Date(), 200, new 
Flags(Flag.ANSWERED), "My Body".getBytes(), headersSubject);
+        index.add(session, mailbox, m);
+        
+        SimpleMailboxMembership m2 = new 
SimpleMailboxMembership(mailbox2.getMailboxId(),1, new Date(), 20, new 
Flags(Flag.ANSWERED), "My Body".getBytes(), headersSubject);
+        index.add(session, mailbox2, m2);
+
+        Calendar cal = Calendar.getInstance();
+        cal.set(1980, 2, 10);
+        SimpleMailboxMembership m3 = new 
SimpleMailboxMembership(mailbox.getMailboxId(),2, cal.getTime(), 20, new 
Flags(Flag.DELETED), "My Otherbody".getBytes(), headersTest);
+        index.add(session, mailbox, m3);
+        
+        Calendar cal2 = Calendar.getInstance();
+        cal2.set(8000, 2, 10);
+        SimpleMailboxMembership m4 = new 
SimpleMailboxMembership(mailbox.getMailboxId(),3, cal2.getTime(), 20, new 
Flags(Flag.DELETED), "My Otherbody2".getBytes(), headersTestSubject);
+        index.add(session, mailbox, m4);
+    }
+    
+    @Test
+    public void testSearchAll() throws Exception {
+        SearchQuery query = new SearchQuery();
+        query.andCriteria(SearchQuery.all());
+        Iterator<Long> it2 = index.search(session, mailbox2, query);
+        assertTrue(it2.hasNext());
+        assertEquals(1, it2.next().longValue(), 1);
+        assertFalse(it2.hasNext());
+    }
+    
+    @Test
+    public void testSearchFlag() throws Exception {
+
+        SearchQuery q = new SearchQuery();
+        q.andCriteria(SearchQuery.flagIsSet(Flag.DELETED));
+        Iterator<Long> it3 = index.search(session, mailbox, q);
+        assertEquals(3, it3.next().longValue(), 1);
+        assertEquals(4, it3.next().longValue(), 1);
+        assertFalse(it3.hasNext());
+    }
+    
+    @Test
+    public void testSearchBody() throws Exception {    
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.bodyContains("body"));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchMail() throws Exception {    
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.mailContains("body"));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchHeaderContains() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.headerContains("Subject", "test"));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchHeaderExists() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.headerExists("Subject"));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(3, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchFlagUnset() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.flagIsUnSet(Flag.DRAFT));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertEquals(3, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    @Test
+    public void testSearchInternalDateBefore() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(new Date());
+        
q2.andCriteria(SearchQuery.internalDateBefore(cal.get(Calendar.DAY_OF_MONTH), 
cal.get(Calendar.MONTH) +1 , cal.get(Calendar.YEAR)));
+        
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    @Test
+    public void testSearchInternalDateAfter() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(new Date());
+        
q2.andCriteria(SearchQuery.internalDateAfter(cal.get(Calendar.DAY_OF_MONTH), 
cal.get(Calendar.MONTH) +1, cal.get(Calendar.YEAR)));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(3, it4.next().longValue(), 1);
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    
+    @Test
+    public void testSearchInternalDateOn() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(new Date());
+        
q2.andCriteria(SearchQuery.internalDateOn(cal.get(Calendar.DAY_OF_MONTH), 
cal.get(Calendar.MONTH) +1, cal.get(Calendar.YEAR)));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchUidMatch() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(new Date());
+        q2.andCriteria(SearchQuery.uid(new SearchQuery.NumericRange[] {new 
SearchQuery.NumericRange(1)}));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next(), 1);
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    @Test
+    public void testSearchUidRange() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(new Date());
+        q2.andCriteria(SearchQuery.uid(new SearchQuery.NumericRange[] {new 
SearchQuery.NumericRange(1), new SearchQuery.NumericRange(2,3)}));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertEquals(3, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    
+    @Test
+    public void testSearchSizeEquals() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.sizeEquals(200));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    @Test
+    public void testSearchSizeLessThan() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.sizeLessThan(200));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertEquals(3, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    
+    
+    @Test
+    public void testSearchSizeGreaterThan() throws Exception {
+        SearchQuery q2 = new SearchQuery();
+        q2.andCriteria(SearchQuery.sizeGreaterThan(6));
+        Iterator<Long> it4 = index.search(session, mailbox, q2);
+        assertEquals(1, it4.next().longValue(), 1);
+        assertEquals(2, it4.next().longValue(), 1);
+        assertEquals(3, it4.next().longValue(), 1);
+
+        assertFalse(it4.hasNext());
+    }
+    private final class SimpleMailbox implements Mailbox<Long> {
+        private long id;
+
+        public SimpleMailbox(long id) {
+            this.id = id;
+        }
+
+        public Long getMailboxId() {
+            return id;
+        }
+
+        public String getNamespace() {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        public void setNamespace(String namespace) {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        public String getUser() {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        public void setUser(String user) {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        public String getName() {
+            return Long.toString(id);
+        }
+
+        public void setName(String name) {
+            throw new UnsupportedOperationException("Not supported");
+
+        }
+
+        public long getUidValidity() {
+            return 0;
+        }
+    }
+}



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to