/* ** Copyright (C) 2021 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the ** Free Software Foundation; either version 3, or (at your option) any ** later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software Foundation, ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** */ #ifndef MU_QUERY_RESULTS_HH__ #define MU_QUERY_RESULTS_HH__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mu-msg.hh" namespace Mu { /** * This implements a QueryResults structure, which capture the results of a * Xapian query, and a QueryResultsIterator, which gives C++-compliant iterator * to go over the results. and finally QueryThreader (in query-threader.cc) which * calculates the threads, using the JWZ algorithm. */ /// Flags that influence now matches are presented (or skipped) enum struct QueryFlags { None = 0, /**< no flags */ Descending = 1 << 0, /**< sort z->a */ SkipUnreadable = 1 << 1, /**< skip unreadable msgs */ SkipDuplicates = 1 << 2, /**< skip duplicate msgs */ IncludeRelated = 1 << 3, /**< include related msgs */ Threading = 1 << 4, /**< calculate threading info */ // internal Leader = 1 << 5, /**< This is the leader query (for internal use * only)*/ }; MU_ENABLE_BITOPS (QueryFlags); /// Stores all the essential information for sorting the results. struct QueryMatch { /// Flags for a match (message) found enum struct Flags { None = 0, /**< No Flags */ Leader = 1 << 0, /**< Mark direct matches as leader */ Related = 1 << 1, /**< A related message */ Unreadable = 1 << 2, /**< No readable file */ Duplicate = 1 << 3, /**< Message-id seen before */ Root = 1 << 10, /**< Is this the thread-root? */ First = 1 << 11, /**< Is this the first message in a thread? */ Last = 1 << 12, /**< Is this the last message in a thread? */ Orphan = 1 << 13, /**< Is this message without a parent? */ HasChild = 1 << 14, /**< Does this message have a child? */ ThreadSubject = 1 << 20, /**< Message holds subject for (sub)thread */ }; Flags flags{Flags::None}; /**< Flags */ std::string date_key; /**< The date-key (for sorting all sub-root levels) */ // the thread subject is the subject of the first message in a thread, // and any message that has a different subject compared to its predecessor // (ignoring prefixes such as Re:) // // otherwise, it is empty. std::string subject; /**< subject for this message */ size_t thread_level{}; /**< The thread level */ std::string thread_path; /**< The hex-numerial path in the thread, ie. '00:01:0a' */ std::string thread_date; /**< date of newest message in thread */ bool operator< (const QueryMatch &rhs) const { return date_key < rhs.date_key; } bool has_flag (Flags flag) const; }; MU_ENABLE_BITOPS (QueryMatch::Flags); inline bool QueryMatch::has_flag (QueryMatch::Flags flag) const { return any_of (flags & flag); } inline std::ostream & operator<< (std::ostream &os, QueryMatch::Flags mflags) { if (mflags == QueryMatch::Flags::None) { os << ""; return os; } if (any_of (mflags & QueryMatch::Flags::Leader)) os << "leader "; if (any_of (mflags & QueryMatch::Flags::Unreadable)) os << "unreadable "; if (any_of (mflags & QueryMatch::Flags::Duplicate)) os << "dup "; if (any_of (mflags & QueryMatch::Flags::Root)) os << "root "; if (any_of (mflags & QueryMatch::Flags::Related)) os << "related "; if (any_of (mflags & QueryMatch::Flags::First)) os << "first "; if (any_of (mflags & QueryMatch::Flags::Last)) os << "last "; if (any_of (mflags & QueryMatch::Flags::Orphan)) os << "orphan "; if (any_of (mflags & QueryMatch::Flags::HasChild)) os << "has-child "; return os; } using QueryMatches = std::unordered_map; inline std::ostream & operator<< (std::ostream &os, const QueryMatch &qmatch) { os << "qm:[" << qmatch.thread_path << "]: " // " (" << qmatch.thread_level << "): " << "> date:<" << qmatch.date_key << "> " << "flags:{" << qmatch.flags << "}"; return os; } /// /// This is a view over the Xapian::MSet, which can optionally filter unreadable /// / duplicate messages. /// /// Note, we internally skip unreadable/duplicate messages (when asked too); those /// skipped ones do _not_ count towards the max_size /// class QueryResultsIterator { public: using iterator_category = std::output_iterator_tag; using value_type = MuMsg *; using difference_type = void; using pointer = void; using reference = void; QueryResultsIterator (Xapian::MSetIterator mset_it, QueryMatches &query_matches) : mset_it_{mset_it}, query_matches_{query_matches} { } ~QueryResultsIterator() { g_clear_pointer (&msg_, mu_msg_unref); } /** * Increment the iterator (we don't support post-increment) * * @return an updated iterator, or end() if we were already at end() */ QueryResultsIterator &operator++() { ++mset_it_; return *this; } /** * (Non)Equivalence operators * * @param rhs some other iterator * * @return true or false */ bool operator== (const QueryResultsIterator &rhs) const { return mset_it_ == rhs.mset_it_; } bool operator!= (const QueryResultsIterator &rhs) const { return mset_it_ != rhs.mset_it_; } QueryResultsIterator & operator*() { return *this; } const QueryResultsIterator &operator*() const { return *this; } /** * Get the Xapian document this iterator is pointing at, * or an empty document when looking at end(). * * @return a document */ Xapian::Document document() const { return mset_it_.get_document(); } /** * Get the doc-id for the document this iterator is pointing at, or 0 * when looking at end. * * @return a doc-id. */ Xapian::docid doc_id() const { return *mset_it_; } /** * Get the message-id for the document (message) this iterator is * pointing at, or not when not available * * @return a message-id */ Option message_id() const noexcept { return opt_string (MU_MSG_FIELD_ID_MSGID); } /** * Get the thread-id for the document (message) this iterator is * pointing at, or Nothing. * * @return a message-id */ Option thread_id() const noexcept { return opt_string (MU_MSG_FIELD_ID_THREAD_ID); } /** * Get the file-system path for the document (message) this iterator is * pointing at, or Nothing. * * @return a filesystem path */ Option path() const noexcept { return opt_string (MU_MSG_FIELD_ID_PATH); } /** * Get the date for the document (message) the iterator is pointing at. * pointing at, or Nothing. * * @return a filesystem path */ Option date() const noexcept { return opt_string (MU_MSG_FIELD_ID_DATE); } /** * Get the file-system path for the document (message) this iterator is * pointing at. * * @return the subject */ Option subject() const noexcept { return opt_string (MU_MSG_FIELD_ID_SUBJECT); } /** * Get the references for the document (messages) this is iterator is * pointing at, or empty if pointing at end of if no references are * available. * * @return references */ std::vector references() const noexcept { return split (document().get_value (MU_MSG_FIELD_ID_REFS), ","); } /** * Get some value from the document, or Nothing if empty. * * @param id a message field id * * @return the value */ Option opt_string (MuMsgFieldId id) const noexcept try { auto &&val{document().get_value (id)}; return val.empty() ? Nothing : Some (val); } MU_XAPIAN_CATCH_BLOCK_RETURN (Nothing); /** * Get the Query match info for this message. * * @return the match info. */ QueryMatch &query_match() { g_assert (query_matches_.find (document().get_docid()) != query_matches_.end()); return query_matches_.find (document().get_docid())->second; } const QueryMatch &query_match() const { g_assert (query_matches_.find (document().get_docid()) != query_matches_.end()); return query_matches_.find (document().get_docid())->second; } /** * get the corresponding MuMsg for this iter; this instance is owned by * @this, and becomes invalid when iterating to the next, or @this is k * destroyed.; it's a 'floating' reference. * * @return a MuMsg* or NUL in case of error */ MuMsg *floating_msg() G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT try { auto docp{reinterpret_cast (new Xapian::Document (document()))}; GError *err{}; g_clear_pointer (&msg_, mu_msg_unref); if (!(msg_ = mu_msg_new_from_doc (docp, &err))) { delete docp; g_warning ("failed to crate message for %s: %s", path().value_or ("").c_str(), err ? err->message : "somethng went wrong"); g_clear_error (&err); } return msg_; } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); private: Xapian::MSetIterator mset_it_; QueryMatches & query_matches_; MuMsg * msg_{}; }; constexpr auto MaxQueryResultsSize = std::numeric_limits::max(); class QueryResults { public: /// Helper types using iterator = QueryResultsIterator; using const_iterator = const iterator; /** * Construct a QueryResults object * * @param mset an Xapian::MSet with matches */ QueryResults (const Xapian::MSet &mset, QueryMatches &&query_matches) : mset_{mset}, query_matches_{std::move (query_matches)} { } /** * Is this QueryResults object empty (ie., no matches)? * * @return true are false */ bool empty() const { return mset_.empty(); } /** * Get the number of matches in this QueryResult * * @return number of matches */ size_t size() const { return mset_.size(); } /** * Get the begin iterator to the results. * * @return iterator */ iterator begin() { return QueryResultsIterator (mset_.begin(), query_matches_); } const iterator begin() const { return QueryResultsIterator (mset_.begin(), query_matches_); } /** * Get the end iterator to the results. * * @return iterator */ iterator end() { return QueryResultsIterator (mset_.end(), query_matches_); } const_iterator end() const { return QueryResultsIterator (mset_.end(), query_matches_); } /** * Get the query-matches for these QueryResults. The non-const * version can be use to _steal_ the query results, by moving * them. * * @return query-matches */ const QueryMatches &query_matches() const { return query_matches_; } QueryMatches & query_matches() { return query_matches_; } private: const Xapian::MSet mset_; mutable QueryMatches query_matches_; }; } // namespace Mu #endif /* MU_QUERY_RESULTS_HH__ */