[geary/wip/api-search] Properly break-out SearchFolder ImapDB internals from API class



commit 339e3a74d962cf94def6ea992d6ac25655b3db10
Author: Jim Nelson <jim yorba org>
Date:   Fri Feb 27 14:49:01 2015 -0800

    Properly break-out SearchFolder ImapDB internals from API class

 src/CMakeLists.txt                                 |    9 +-
 src/engine/api/geary-search-folder.vala            |  443 ++------------------
 src/engine/imap-db/imap-db-account.vala            |    2 +-
 .../imap-db-search-email-identifier.vala           |   16 +-
 .../search/imap-db-search-folder-properties.vala   |   16 +
 .../imap-db/search/imap-db-search-folder-root.vala |   14 +
 .../imap-db/search/imap-db-search-folder.vala      |  404 ++++++++++++++++++
 .../imap-db/{ => search}/imap-db-search-query.vala |    0
 .../imap-db/{ => search}/imap-db-search-term.vala  |    0
 .../gmail/imap-engine-gmail-search-folder.vala     |    4 +-
 .../imap-engine/imap-engine-generic-account.vala   |    4 +-
 11 files changed, 488 insertions(+), 424 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 78151fc..102917e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -172,9 +172,12 @@ engine/imap-db/imap-db-folder.vala
 engine/imap-db/imap-db-gc.vala
 engine/imap-db/imap-db-message-addresses.vala
 engine/imap-db/imap-db-message-row.vala
-engine/imap-db/imap-db-search-query.vala
-engine/imap-db/imap-db-search-term.vala
-engine/imap-db/imap-db-search-email-identifier.vala
+engine/imap-db/search/imap-db-search-email-identifier.vala
+engine/imap-db/search/imap-db-search-folder.vala
+engine/imap-db/search/imap-db-search-folder-properties.vala
+engine/imap-db/search/imap-db-search-folder-root.vala
+engine/imap-db/search/imap-db-search-query.vala
+engine/imap-db/search/imap-db-search-term.vala
 engine/imap-db/outbox/smtp-outbox-email-identifier.vala
 engine/imap-db/outbox/smtp-outbox-email-properties.vala
 engine/imap-db/outbox/smtp-outbox-folder.vala
diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala
index 92363ae..af2d70d 100644
--- a/src/engine/api/geary-search-folder.vala
+++ b/src/engine/api/geary-search-folder.vala
@@ -4,44 +4,30 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
-private class Geary.SearchFolderRoot : Geary.FolderRoot {
-    public const string MAGIC_BASENAME = "$GearySearchFolder$";
-    
-    public SearchFolderRoot() {
-        base(MAGIC_BASENAME, null, false, false);
-    }
-}
-
-private class Geary.SearchFolderProperties : Geary.FolderProperties {
-    public SearchFolderProperties(int total, int unread) {
-        base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
-    }
-    
-    public void set_total(int total) {
-        this.email_total = total;
-    }
-}
-
 /**
- * Special folder type used to query and display search results.
+ * Special local { link Folder} used to query and display search results of { link Email} from
+ * across the { link Account}'s local storage.
+ *
+ * SearchFolder is merely specified to be a Folder, but implementations may add various
+ * { link FolderSupport} interfaces.  In particular { link FolderSupport.Remove} should be supported,
+ * but again, is not required.
+ *
+ * SearchFolder is expected to produce { link EmailIdentifier}s which can be accepted by other
+ * Folders within the Account (with the exception of the Outbox).  Those Folders may need to
+ * translate those EmailIdentifiers to their own type for ordering reasons, but in general the
+ * expectation is that the results of SearchFolder can then be applied to operations on Email in
+ * other remote-backed folders.
  */
 
-public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport.Remove {
-    // Max number of emails that can ever be in the folder.
-    public static const int MAX_RESULT_EMAILS = 1000;
-    
+public abstract class Geary.SearchFolder : Geary.AbstractLocalFolder {
     private weak Account _account;
     public override Account account { get { return _account; } }
     
-    private SearchFolderProperties _properties = new SearchFolderProperties(0, 0);
+    private FolderProperties _properties;
     public override FolderProperties properties { get { return _properties; } }
     
     private FolderPath? _path = null;
-    public override FolderPath path {
-        get {
-            return (_path != null) ? _path : _path = new SearchFolderRoot();
-        }
-    }
+    public override FolderPath path { get { return _path; } }
     
     public override SpecialFolderType special_folder_type {
         get {
@@ -49,17 +35,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
         }
     }
     
-    public Geary.SearchQuery? search_query { get; private set; default = null; }
-    
-    private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
-    private Geary.SpecialFolderType[] exclude_types = {
-        Geary.SpecialFolderType.SPAM,
-        Geary.SpecialFolderType.TRASH,
-        Geary.SpecialFolderType.DRAFTS,
-        // Orphan emails (without a folder) are also excluded; see ctor.
-    };
-    private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
-    private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
+    public Geary.SearchQuery? search_query { get; protected set; default = null; }
     
     /**
      * Fired when the search query has changed.  This signal is fired *after* the search
@@ -67,388 +43,39 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
      */
     public signal void search_query_changed(Geary.SearchQuery? query);
     
-    public SearchFolder(Account account) {
-        base();
-        
+    protected SearchFolder(Account account, FolderProperties properties, FolderPath path) {
         _account = account;
-        
-        account.folders_available_unavailable.connect(on_folders_available_unavailable);
-        account.email_locally_complete.connect(on_email_locally_complete);
-        account.email_removed.connect(on_account_email_removed);
-        
-        clear_search_results();
-        
-        // We always want to exclude emails that don't live anywhere from
-        // search results.
-        exclude_orphan_emails();
+        _properties = properties;
+        _path = path;
     }
     
-    ~SearchFolder() {
-        account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
-        account.email_locally_complete.disconnect(on_email_locally_complete);
-        account.email_removed.disconnect(on_account_email_removed);
-    }
-    
-    private void on_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
-        Gee.Collection<Geary.Folder>? unavailable) {
-        if (available != null) {
-            // Exclude it from searching if it's got the right special type.
-            foreach(Geary.Folder folder in Geary.traverse<Geary.Folder>(available)
-                .filter(f => f.special_folder_type in exclude_types))
-                exclude_folder(folder);
-        }
-    }
-    
-    public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
-        out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
-        Cancellable? cancellable = null) throws Error {
-        low = null;
-        high = null;
-        
-        // This shouldn't require a result_mutex lock since there's no yield.
-        Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = Geary.traverse<Geary.EmailIdentifier>(ids)
-            .cast_object<ImapDB.SearchEmailIdentifier>()
-            .filter(id => id in search_results)
-            .to_tree_set();
-        
-        if (in_folder.size > 0) {
-            low = in_folder.first();
-            high = in_folder.last();
-        }
-    }
-    
-    private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
-        int result_mutex_token = yield result_mutex.claim_async();
-        
-        Error? error = null;
-        try {
-            yield do_search_async(query, ids, null, cancellable);
-        } catch(Error e) {
-            error = e;
-        }
-        
-        result_mutex.release(ref result_mutex_token);
-        
-        if (error != null)
-            throw error;
-    }
-    
-    private void on_append_new_email_complete(Object? source, AsyncResult result) {
-        try {
-            append_new_email_async.end(result);
-        } catch(Error e) {
-            debug("Error appending new email to search results: %s", e.message);
-        }
-    }
-    
-    private void on_email_locally_complete(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> ids) {
-        if (search_query != null)
-            append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete);
-    }
-    
-    private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
-        int result_mutex_token = yield result_mutex.claim_async();
-        
-        Error? error = null;
-        try {
-            Gee.ArrayList<ImapDB.SearchEmailIdentifier> relevant_ids
-                = Geary.traverse<Geary.EmailIdentifier>(ids)
-                .map_nonnull<ImapDB.SearchEmailIdentifier>(
-                    id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id))
-                .to_array_list();
-            
-            if (relevant_ids.size > 0)
-                yield do_search_async(query, null, relevant_ids, cancellable);
-        } catch(Error e) {
-            error = e;
-        }
-        
-        result_mutex.release(ref result_mutex_token);
-        
-        if (error != null)
-            throw error;
-    }
-    
-    private void on_handle_removed_email_complete(Object? source, AsyncResult result) {
-        try {
-            handle_removed_email_async.end(result);
-        } catch(Error e) {
-            debug("Error removing removed email from search results: %s", e.message);
-        }
-    }
-    
-    private void on_account_email_removed(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> ids) {
-        if (search_query != null)
-            handle_removed_email_async.begin(search_query, folder, ids, null, 
on_handle_removed_email_complete);
+    protected virtual void notify_search_query_changed(SearchQuery? query) {
+        search_query_changed(query);
     }
     
     /**
-     * Clears the search query and results.
+     * Sets the keyword string for this search.
+     *
+     * This is a nonblocking call that initiates a background search which can be stopped with the
+     * supplied Cancellable.
+     *
+     * When the search is completed, { link search_query_changed} will be fired.  It's possible for
+     * the { link search_query} property to change before completion.
      */
-    public void clear() {
-        Gee.Collection<ImapDB.SearchEmailIdentifier> local_results = search_results;
-        clear_search_results();
-        notify_email_removed(local_results);
-        notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
-        
-        if (search_query != null) {
-            search_query = null;
-            search_query_changed(null);
-        }
-    }
+    public abstract void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = 
null);
     
     /**
-     * Sets the keyword string for this search.
+     * Clears the search query and results.
+     *
+     * { link search_query_changed} will be fired and { link search_query} will be set to null.
      */
-    public void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = null) {
-        set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
-    }
-    
-    private void on_set_search_query_complete(Object? source, AsyncResult result) {
-        try {
-            set_search_query_async.end(result);
-        } catch(Error e) {
-            debug("Search error: %s", e.message);
-        }
-    }
-    
-    private async void set_search_query_async(string query, SearchQuery.Strategy strategy,
-        Cancellable? cancellable) throws Error {
-        Geary.SearchQuery search_query = account.open_search(query, strategy);
-        
-        int result_mutex_token = yield result_mutex.claim_async();
-        
-        Error? error = null;
-        try {
-            yield do_search_async(search_query, null, null, cancellable);
-        } catch(Error e) {
-            error = e;
-        }
-        
-        result_mutex.release(ref result_mutex_token);
-        
-        this.search_query = search_query;
-        search_query_changed(search_query);
-        
-        if (error != null)
-            throw error;
-    }
-    
-    // NOTE: you must call this ONLY after locking result_mutex_token.
-    // If both *_ids parameters are null, the results of this search are
-    // considered to be the full new set.  If non-null, the results are
-    // considered to be a delta and are added or subtracted from the full set.
-    // add_ids are new ids to search for, remove_ids are ids in our result set
-    // that will be removed if this search doesn't turn them up.
-    private async void do_search_async(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>? 
add_ids,
-        Gee.Collection<ImapDB.SearchEmailIdentifier>? remove_ids, Cancellable? cancellable) throws Error {
-        // There are three cases here: 1) replace full result set, where the
-        // *_ids parameters are both null, 2) add to result set, where just
-        // remove_ids is null, and 3) remove from result set, where just
-        // add_ids is null.  We can't add and remove at the same time.
-        assert(add_ids == null || remove_ids == null);
-        
-        // TODO: don't limit this to MAX_RESULT_EMAILS.  Instead, we could be
-        // smarter about only fetching the search results in list_email_async()
-        // etc., but this leads to some more complications when redoing the
-        // search.
-        Gee.ArrayList<ImapDB.SearchEmailIdentifier> results
-            = ImapDB.SearchEmailIdentifier.array_list_from_results(yield account.local_search_async(
-            query, MAX_RESULT_EMAILS, 0, exclude_folders, add_ids ?? remove_ids, cancellable));
-        
-        Gee.List<ImapDB.SearchEmailIdentifier> added
-            = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
-        Gee.List<ImapDB.SearchEmailIdentifier> removed
-            = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
-        
-        if (remove_ids == null) {
-            added = Geary.traverse<ImapDB.SearchEmailIdentifier>(results)
-                .filter(id => !(id in search_results))
-                .to_array_list();
-        }
-        if (add_ids == null) {
-            removed = Geary.traverse<ImapDB.SearchEmailIdentifier>(remove_ids ?? search_results)
-                .filter(id => !(id in results))
-                .to_array_list();
-        }
-        
-        search_results.remove_all(removed);
-        search_results.add_all(added);
-        
-        _properties.set_total(search_results.size);
-        
-        // Note that we probably shouldn't be firing these signals from inside
-        // our mutex lock.  Keep an eye on it, and if there's ever a case where
-        // it might cause problems, it shouldn't be too hard to move the
-        // firings outside.
-        
-        Geary.Folder.CountChangeReason reason = CountChangeReason.NONE;
-        if (added.size > 0) {
-            // TODO: we'd like to be able to use APPENDED here when applicable,
-            // but because of the potential to append a thousand results at
-            // once and the ConversationMonitor's inability to handle that
-            // gracefully (#7464), we always use INSERTED for now.
-            notify_email_inserted(added);
-            reason |= Geary.Folder.CountChangeReason.INSERTED;
-        }
-        if (removed.size > 0) {
-            notify_email_removed(removed);
-            reason |= Geary.Folder.CountChangeReason.REMOVED;
-        }
-        if (reason != CountChangeReason.NONE)
-            notify_email_count_changed(search_results.size, reason);
-    }
-    
-    public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
-        int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = 
null)
-        throws Error {
-        if (count <= 0)
-            return null;
-        
-        // TODO: as above, this is incomplete and inefficient.
-        int result_mutex_token = yield result_mutex.claim_async();
-        
-        Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size];
-        int initial_index = 0;
-        int i = 0;
-        foreach (ImapDB.SearchEmailIdentifier id in search_results) {
-            if (initial_id != null && id.equal_to(initial_id))
-                initial_index = i;
-            ids[i++] = id;
-        }
-        
-        if (initial_id == null && flags.is_all_set(Folder.ListFlags.OLDEST_TO_NEWEST))
-            initial_index = ids.length - 1;
-        
-        Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
-        Error? fetch_err = null;
-        if (initial_index >= 0) {
-            int increment = flags.is_oldest_to_newest() ? -1 : 1;
-            i = initial_index;
-            if (!flags.is_including_id() && initial_id != null)
-                i += increment;
-            int end = i + (count * increment);
-            
-            for (; i >= 0 && i < search_results.size && i != end; i += increment) {
-                try {
-                    results.add(yield fetch_email_async(ids[i], required_fields, flags, cancellable));
-                } catch (Error err) {
-                    // Don't let missing or incomplete messages stop the list operation, which has
-                    // different symantics from fetch
-                    if (!(err is EngineError.NOT_FOUND) && !(err is EngineError.INCOMPLETE_MESSAGE)) {
-                        fetch_err = err;
-                        
-                        break;
-                    }
-                }
-            }
-        }
-        
-        result_mutex.release(ref result_mutex_token);
-        
-        if (fetch_err != null)
-            throw fetch_err;
-        
-        return (results.size == 0 ? null : results);
-    }
-    
-    public override async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
-        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
-        Cancellable? cancellable = null) throws Error {
-        // TODO: Fetch emails in a batch.
-        Gee.List<Geary.Email> result = new Gee.ArrayList<Geary.Email>();
-        foreach(Geary.EmailIdentifier id in ids)
-            result.add(yield fetch_email_async(id, required_fields, flags, cancellable));
-        
-        return (result.size == 0 ? null : result);
-    }
-    
-    public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
-        // TODO: This method is not currently called, but is required by the interface.  Before completing
-        // this feature, it should either be implemented either here or in AbstractLocalFolder. 
-        error("Search folder does not implement list_local_email_fields_async");
-    }
-    
-    public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
-        Geary.Email.Field required_fields, Geary.Folder.ListFlags flags,
-        Cancellable? cancellable = null) throws Error {
-        return yield account.local_fetch_email_async(id, required_fields, cancellable);
-    }
-    
-    public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
-        Cancellable? cancellable = null) throws Error {
-        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders
-            = yield account.get_containing_folders_async(email_ids, cancellable);
-        if (ids_to_folders == null)
-            return;
-        
-        Gee.MultiMap<Geary.FolderPath, Geary.EmailIdentifier> folders_to_ids
-            = Geary.Collection.reverse_multi_map<Geary.EmailIdentifier, Geary.FolderPath>(ids_to_folders);
-        
-        foreach (Geary.FolderPath path in folders_to_ids.get_keys()) {
-            Geary.Folder folder = yield account.fetch_folder_async(path, cancellable);
-            Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
-            if (remove == null)
-                continue;
-            
-            Gee.Collection<Geary.EmailIdentifier> ids = folders_to_ids.get(path);
-            assert(ids.size > 0);
-            
-            debug("Search folder removing %d emails from %s", ids.size, folder.to_string());
-            
-            bool open = false;
-            try {
-                yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable);
-                open = true;
-                
-                yield remove.remove_email_async(
-                    Geary.Collection.to_array_list<Geary.EmailIdentifier>(ids), cancellable);
-                
-                yield folder.close_async(cancellable);
-                open = false;
-            } catch (Error e) {
-                debug("Error removing messages in %s: %s", folder.to_string(), e.message);
-                
-                if (open) {
-                    try {
-                        yield folder.close_async(cancellable);
-                        open = false;
-                    } catch (Error e) {
-                        debug("Error closing folder %s: %s", folder.to_string(), e.message);
-                    }
-                }
-            }
-        }
-    }
+    public abstract void clear();
     
     /**
      * Given a list of mail IDs, returns a set of casefolded words that match for the current
      * search query.
      */
-    public async Gee.Set<string>? get_search_matches_async(
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
-        if (search_query == null)
-            return null;
-        
-        return yield account.get_search_matches_async(search_query, ids, cancellable);
-    }
-    
-    private void exclude_folder(Geary.Folder folder) {
-        exclude_folders.add(folder.path);
-    }
-    
-    private void exclude_orphan_emails() {
-        exclude_folders.add(null);
-    }
-    
-    private void clear_search_results() {
-        search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
-            ImapDB.SearchEmailIdentifier.compare_descending);
-    }
+    public abstract async Gee.Set<string>? get_search_matches_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
 }
 
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index 9366ea0..9e29cfa 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -27,7 +27,7 @@ private class Geary.ImapDB.Account : BaseObject {
     
     // Only available when the Account is opened
     public SmtpOutboxFolder? outbox { get; private set; default = null; }
-    public SearchFolder? search_folder { get; private set; default = null; }
+    public Geary.SearchFolder? search_folder { get; private set; default = null; }
     public ImapEngine.ContactStore contact_store { get; private set; }
     public IntervalProgressMonitor search_index_monitor { get; private set; 
         default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
diff --git a/src/engine/imap-db/imap-db-search-email-identifier.vala 
b/src/engine/imap-db/search/imap-db-search-email-identifier.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-email-identifier.vala
rename to src/engine/imap-db/search/imap-db-search-email-identifier.vala
index fceb1ab..6df1bef 100644
--- a/src/engine/imap-db/imap-db-search-email-identifier.vala
+++ b/src/engine/imap-db/search/imap-db-search-email-identifier.vala
@@ -6,6 +6,14 @@
 
 private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
     Gee.Comparable<SearchEmailIdentifier> {
+    public DateTime? date_received { get; private set; }
+    
+    public SearchEmailIdentifier(int64 message_id, DateTime? date_received) {
+        base(message_id, null);
+        
+        this.date_received = date_received;
+    }
+    
     public static int compare_descending(SearchEmailIdentifier a, SearchEmailIdentifier b) {
         return b.compare_to(a);
     }
@@ -36,14 +44,6 @@ private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
         return null;
     }
     
-    public DateTime? date_received { get; private set; }
-    
-    public SearchEmailIdentifier(int64 message_id, DateTime? date_received) {
-        base(message_id, null);
-        
-        this.date_received = date_received;
-    }
-    
     public override int natural_sort_comparator(Geary.EmailIdentifier o) {
         ImapDB.SearchEmailIdentifier? other = o as ImapDB.SearchEmailIdentifier;
         if (other == null)
diff --git a/src/engine/imap-db/search/imap-db-search-folder-properties.vala 
b/src/engine/imap-db/search/imap-db-search-folder-properties.vala
new file mode 100644
index 0000000..4b64823
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder-properties.vala
@@ -0,0 +1,16 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+private class Geary.ImapDB.SearchFolderProperties : Geary.FolderProperties {
+    public SearchFolderProperties(int total, int unread) {
+        base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
+    }
+    
+    public void set_total(int total) {
+        this.email_total = total;
+    }
+}
+
diff --git a/src/engine/imap-db/search/imap-db-search-folder-root.vala 
b/src/engine/imap-db/search/imap-db-search-folder-root.vala
new file mode 100644
index 0000000..8b8109f
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder-root.vala
@@ -0,0 +1,14 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+private class Geary.ImapDB.SearchFolderRoot : Geary.FolderRoot {
+    public const string MAGIC_BASENAME = "$GearySearchFolder$";
+    
+    public SearchFolderRoot() {
+        base(MAGIC_BASENAME, null, false, false);
+    }
+}
+
diff --git a/src/engine/imap-db/search/imap-db-search-folder.vala 
b/src/engine/imap-db/search/imap-db-search-folder.vala
new file mode 100644
index 0000000..a48631f
--- /dev/null
+++ b/src/engine/imap-db/search/imap-db-search-folder.vala
@@ -0,0 +1,404 @@
+/* Copyright 2015 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+private class Geary.ImapDB.SearchFolder : Geary.SearchFolder, Geary.FolderSupport.Remove {
+    // Max number of emails that can ever be in the folder.
+    public const int MAX_RESULT_EMAILS = 1000;
+    
+    private const Geary.SpecialFolderType[] exclude_types = {
+        Geary.SpecialFolderType.SPAM,
+        Geary.SpecialFolderType.TRASH,
+        Geary.SpecialFolderType.DRAFTS,
+        // Orphan emails (without a folder) are also excluded; see ctor.
+    };
+    
+    private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
+    private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
+    private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
+    
+    public SearchFolder(Geary.Account account) {
+        base (account, new SearchFolderProperties(0, 0), new SearchFolderRoot());
+        
+        account.folders_available_unavailable.connect(on_folders_available_unavailable);
+        account.email_locally_complete.connect(on_email_locally_complete);
+        account.email_removed.connect(on_account_email_removed);
+        
+        clear_search_results();
+        
+        // We always want to exclude emails that don't live anywhere from
+        // search results.
+        exclude_orphan_emails();
+    }
+    
+    ~SearchFolder() {
+        account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
+        account.email_locally_complete.disconnect(on_email_locally_complete);
+        account.email_removed.disconnect(on_account_email_removed);
+    }
+    
+    private void on_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
+        Gee.Collection<Geary.Folder>? unavailable) {
+        if (available != null) {
+            // Exclude it from searching if it's got the right special type.
+            foreach(Geary.Folder folder in Geary.traverse<Geary.Folder>(available)
+                .filter(f => f.special_folder_type in exclude_types))
+                exclude_folder(folder);
+        }
+    }
+    
+    public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
+        out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+        Cancellable? cancellable = null) throws Error {
+        low = null;
+        high = null;
+        
+        // This shouldn't require a result_mutex lock since there's no yield.
+        Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = Geary.traverse<Geary.EmailIdentifier>(ids)
+            .cast_object<ImapDB.SearchEmailIdentifier>()
+            .filter(id => id in search_results)
+            .to_tree_set();
+        
+        if (in_folder.size > 0) {
+            low = in_folder.first();
+            high = in_folder.last();
+        }
+    }
+    
+    private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+        
+        Error? error = null;
+        try {
+            yield do_search_async(query, ids, null, cancellable);
+        } catch(Error e) {
+            error = e;
+        }
+        
+        result_mutex.release(ref result_mutex_token);
+        
+        if (error != null)
+            throw error;
+    }
+    
+    private void on_append_new_email_complete(Object? source, AsyncResult result) {
+        try {
+            append_new_email_async.end(result);
+        } catch(Error e) {
+            debug("Error appending new email to search results: %s", e.message);
+        }
+    }
+    
+    private void on_email_locally_complete(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids) {
+        if (search_query != null)
+            append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete);
+    }
+    
+    private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
+        int result_mutex_token = yield result_mutex.claim_async();
+        
+        Error? error = null;
+        try {
+            Gee.ArrayList<ImapDB.SearchEmailIdentifier> relevant_ids
+                = Geary.traverse<Geary.EmailIdentifier>(ids)
+                .map_nonnull<ImapDB.SearchEmailIdentifier>(
+                    id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id))
+                .to_array_list();
+            
+            if (relevant_ids.size > 0)
+                yield do_search_async(query, null, relevant_ids, cancellable);
+        } catch(Error e) {
+            error = e;
+        }
+        
+        result_mutex.release(ref result_mutex_token);
+        
+        if (error != null)
+            throw error;
+    }
+    
+    private void on_handle_removed_email_complete(Object? source, AsyncResult result) {
+        try {
+            handle_removed_email_async.end(result);
+        } catch(Error e) {
+            debug("Error removing removed email from search results: %s", e.message);
+        }
+    }
+    
+    private void on_account_email_removed(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> ids) {
+        if (search_query != null)
+            handle_removed_email_async.begin(search_query, folder, ids, null, 
on_handle_removed_email_complete);
+    }
+    
+    /**
+     * Clears the search query and results.
+     */
+    public override void clear() {
+        Gee.Collection<ImapDB.SearchEmailIdentifier> local_results = search_results;
+        clear_search_results();
+        notify_email_removed(local_results);
+        notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
+        
+        if (search_query != null) {
+            search_query = null;
+            notify_search_query_changed(null);
+        }
+    }
+    
+    /**
+     * Sets the keyword string for this search.
+     */
+    public override void search(string query, Geary.SearchQuery.Strategy strategy, Cancellable? cancellable 
= null) {
+        set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
+    }
+    
+    private void on_set_search_query_complete(Object? source, AsyncResult result) {
+        try {
+            set_search_query_async.end(result);
+        } catch(Error e) {
+            debug("Search error: %s", e.message);
+        }
+    }
+    
+    private async void set_search_query_async(string query, Geary.SearchQuery.Strategy strategy,
+        Cancellable? cancellable) throws Error {
+        Geary.SearchQuery search_query = account.open_search(query, strategy);
+        
+        int result_mutex_token = yield result_mutex.claim_async();
+        
+        Error? error = null;
+        try {
+            yield do_search_async(search_query, null, null, cancellable);
+        } catch(Error e) {
+            error = e;
+        }
+        
+        result_mutex.release(ref result_mutex_token);
+        
+        this.search_query = search_query;
+        notify_search_query_changed(search_query);
+        
+        if (error != null)
+            throw error;
+    }
+    
+    // NOTE: you must call this ONLY after locking result_mutex_token.
+    // If both *_ids parameters are null, the results of this search are
+    // considered to be the full new set.  If non-null, the results are
+    // considered to be a delta and are added or subtracted from the full set.
+    // add_ids are new ids to search for, remove_ids are ids in our result set
+    // that will be removed if this search doesn't turn them up.
+    private async void do_search_async(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>? 
add_ids,
+        Gee.Collection<ImapDB.SearchEmailIdentifier>? remove_ids, Cancellable? cancellable) throws Error {
+        // There are three cases here: 1) replace full result set, where the
+        // *_ids parameters are both null, 2) add to result set, where just
+        // remove_ids is null, and 3) remove from result set, where just
+        // add_ids is null.  We can't add and remove at the same time.
+        assert(add_ids == null || remove_ids == null);
+        
+        // TODO: don't limit this to MAX_RESULT_EMAILS.  Instead, we could be
+        // smarter about only fetching the search results in list_email_async()
+        // etc., but this leads to some more complications when redoing the
+        // search.
+        Gee.ArrayList<ImapDB.SearchEmailIdentifier> results
+            = ImapDB.SearchEmailIdentifier.array_list_from_results(yield account.local_search_async(
+            query, MAX_RESULT_EMAILS, 0, exclude_folders, add_ids ?? remove_ids, cancellable));
+        
+        Gee.List<ImapDB.SearchEmailIdentifier> added
+            = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
+        Gee.List<ImapDB.SearchEmailIdentifier> removed
+            = Gee.List.empty<ImapDB.SearchEmailIdentifier>();
+        
+        if (remove_ids == null) {
+            added = Geary.traverse<ImapDB.SearchEmailIdentifier>(results)
+                .filter(id => !(id in search_results))
+                .to_array_list();
+        }
+        if (add_ids == null) {
+            removed = Geary.traverse<ImapDB.SearchEmailIdentifier>(remove_ids ?? search_results)
+                .filter(id => !(id in results))
+                .to_array_list();
+        }
+        
+        search_results.remove_all(removed);
+        search_results.add_all(added);
+        
+        ((ImapDB.SearchFolderProperties) properties).set_total(search_results.size);
+        
+        // Note that we probably shouldn't be firing these signals from inside
+        // our mutex lock.  Keep an eye on it, and if there's ever a case where
+        // it might cause problems, it shouldn't be too hard to move the
+        // firings outside.
+        
+        Geary.Folder.CountChangeReason reason = CountChangeReason.NONE;
+        if (added.size > 0) {
+            // TODO: we'd like to be able to use APPENDED here when applicable,
+            // but because of the potential to append a thousand results at
+            // once and the ConversationMonitor's inability to handle that
+            // gracefully (#7464), we always use INSERTED for now.
+            notify_email_inserted(added);
+            reason |= Geary.Folder.CountChangeReason.INSERTED;
+        }
+        if (removed.size > 0) {
+            notify_email_removed(removed);
+            reason |= Geary.Folder.CountChangeReason.REMOVED;
+        }
+        if (reason != CountChangeReason.NONE)
+            notify_email_count_changed(search_results.size, reason);
+    }
+    
+    public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
+        int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable 
= null)
+        throws Error {
+        if (count <= 0)
+            return null;
+        
+        // TODO: as above, this is incomplete and inefficient.
+        int result_mutex_token = yield result_mutex.claim_async();
+        
+        Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size];
+        int initial_index = 0;
+        int i = 0;
+        foreach (ImapDB.SearchEmailIdentifier id in search_results) {
+            if (initial_id != null && id.equal_to(initial_id))
+                initial_index = i;
+            ids[i++] = id;
+        }
+        
+        if (initial_id == null && flags.is_all_set(Geary.Folder.ListFlags.OLDEST_TO_NEWEST))
+            initial_index = ids.length - 1;
+        
+        Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
+        Error? fetch_err = null;
+        if (initial_index >= 0) {
+            int increment = flags.is_oldest_to_newest() ? -1 : 1;
+            i = initial_index;
+            if (!flags.is_including_id() && initial_id != null)
+                i += increment;
+            int end = i + (count * increment);
+            
+            for (; i >= 0 && i < search_results.size && i != end; i += increment) {
+                try {
+                    results.add(yield fetch_email_async(ids[i], required_fields, flags, cancellable));
+                } catch (Error err) {
+                    // Don't let missing or incomplete messages stop the list operation, which has
+                    // different symantics from fetch
+                    if (!(err is EngineError.NOT_FOUND) && !(err is EngineError.INCOMPLETE_MESSAGE)) {
+                        fetch_err = err;
+                        
+                        break;
+                    }
+                }
+            }
+        }
+        
+        result_mutex.release(ref result_mutex_token);
+        
+        if (fetch_err != null)
+            throw fetch_err;
+        
+        return (results.size == 0 ? null : results);
+    }
+    
+    public override async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields,
+        Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error {
+        // TODO: Fetch emails in a batch.
+        Gee.List<Geary.Email> result = new Gee.ArrayList<Geary.Email>();
+        foreach(Geary.EmailIdentifier id in ids)
+            result.add(yield fetch_email_async(id, required_fields, flags, cancellable));
+        
+        return (result.size == 0 ? null : result);
+    }
+    
+    public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
+        // TODO: This method is not currently called, but is required by the interface.  Before completing
+        // this feature, it should either be implemented either here or in AbstractLocalFolder. 
+        error("Search folder does not implement list_local_email_fields_async");
+    }
+    
+    public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
+        Geary.Email.Field required_fields, Geary.Folder.ListFlags flags,
+        Cancellable? cancellable = null) throws Error {
+        return yield account.local_fetch_email_async(id, required_fields, cancellable);
+    }
+    
+    public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+        Cancellable? cancellable = null) throws Error {
+        Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders
+            = yield account.get_containing_folders_async(email_ids, cancellable);
+        if (ids_to_folders == null)
+            return;
+        
+        Gee.MultiMap<Geary.FolderPath, Geary.EmailIdentifier> folders_to_ids
+            = Geary.Collection.reverse_multi_map<Geary.EmailIdentifier, Geary.FolderPath>(ids_to_folders);
+        
+        foreach (Geary.FolderPath path in folders_to_ids.get_keys()) {
+            Geary.Folder folder = yield account.fetch_folder_async(path, cancellable);
+            Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
+            if (remove == null)
+                continue;
+            
+            Gee.Collection<Geary.EmailIdentifier> ids = folders_to_ids.get(path);
+            assert(ids.size > 0);
+            
+            debug("Search folder removing %d emails from %s", ids.size, folder.to_string());
+            
+            bool open = false;
+            try {
+                yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable);
+                open = true;
+                
+                yield remove.remove_email_async(
+                    Geary.Collection.to_array_list<Geary.EmailIdentifier>(ids), cancellable);
+                
+                yield folder.close_async(cancellable);
+                open = false;
+            } catch (Error e) {
+                debug("Error removing messages in %s: %s", folder.to_string(), e.message);
+                
+                if (open) {
+                    try {
+                        yield folder.close_async(cancellable);
+                        open = false;
+                    } catch (Error e) {
+                        debug("Error closing folder %s: %s", folder.to_string(), e.message);
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Given a list of mail IDs, returns a set of casefolded words that match for the current
+     * search query.
+     */
+    public override async Gee.Set<string>? get_search_matches_async(
+        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
+        if (search_query == null)
+            return null;
+        
+        return yield account.get_search_matches_async(search_query, ids, cancellable);
+    }
+    
+    private void exclude_folder(Geary.Folder folder) {
+        exclude_folders.add(folder.path);
+    }
+    
+    private void exclude_orphan_emails() {
+        exclude_folders.add(null);
+    }
+    
+    private void clear_search_results() {
+        search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
+            ImapDB.SearchEmailIdentifier.compare_descending);
+    }
+}
+
diff --git a/src/engine/imap-db/imap-db-search-query.vala 
b/src/engine/imap-db/search/imap-db-search-query.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-query.vala
rename to src/engine/imap-db/search/imap-db-search-query.vala
diff --git a/src/engine/imap-db/imap-db-search-term.vala b/src/engine/imap-db/search/imap-db-search-term.vala
similarity index 100%
rename from src/engine/imap-db/imap-db-search-term.vala
rename to src/engine/imap-db/search/imap-db-search-term.vala
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala 
b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
index c11f0c9..d71d2ae 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
@@ -7,11 +7,11 @@
 /**
  * Gmail-specific SearchFolder implementation.
  */
-private class Geary.ImapEngine.GmailSearchFolder : Geary.SearchFolder {
+private class Geary.ImapEngine.GmailSearchFolder : ImapDB.SearchFolder {
     private Geary.App.EmailStore email_store;
     
     public GmailSearchFolder(Geary.Account account) {
-        base(account);
+        base (account);
         
         email_store = new Geary.App.EmailStore(account);
         
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index d11f3b4..a8aa022 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -46,7 +46,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         }
         
         if (search_path == null) {
-            search_path = new SearchFolderRoot();
+            search_path = new ImapDB.SearchFolderRoot();
         }
     }
     
@@ -247,7 +247,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
     // Subclasses with specific SearchFolder implementations should override
     // this to return the correct subclass.
     internal virtual SearchFolder new_search_folder() {
-        return new SearchFolder(this);
+        return new ImapDB.SearchFolder(this);
     }
     
     private MinimalFolder build_folder(ImapDB.Folder local_folder) {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]