1 /*
2  * This file is part of the KDE Akonadi Search Project
3  * SPDX-FileCopyrightText: 2013 Vishesh Handa <me@vhanda.in>
4  *
5  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6  *
7  */
8 
9 #include "query.h"
10 #include "searchstore.h"
11 #include "term.h"
12 
13 #include <QSharedPointer>
14 #include <QString>
15 #include <QStringList>
16 #include <QUrlQuery>
17 #include <QVariant>
18 
19 #include <QJsonDocument>
20 #include <QJsonObject>
21 
22 using namespace Akonadi::Search;
23 
24 const int defaultLimit = 100000;
25 
26 class Akonadi::Search::QueryPrivate
27 {
28 public:
29     Term m_term;
30 
31     QStringList m_types;
32     QString m_searchString;
33     uint m_limit = defaultLimit;
34     uint m_offset = 0;
35 
36     int m_yearFilter = -1;
37     int m_monthFilter = -1;
38     int m_dayFilter = -1;
39 
40     Query::SortingOption m_sortingOption = Query::SortAuto;
41     QString m_sortingProperty;
42     QVariantMap m_customOptions;
43 };
44 
Query()45 Query::Query()
46     : d(new QueryPrivate)
47 {
48 }
49 
Query(const Term & t)50 Query::Query(const Term &t)
51     : d(new QueryPrivate)
52 {
53     d->m_term = t;
54 }
55 
Query(const Query & rhs)56 Query::Query(const Query &rhs)
57     : d(new QueryPrivate(*rhs.d))
58 {
59 }
60 
61 Query::~Query() = default;
62 
setTerm(const Term & t)63 void Query::setTerm(const Term &t)
64 {
65     d->m_term = t;
66 }
67 
term() const68 Term Query::term() const
69 {
70     return d->m_term;
71 }
72 
addType(const QString & type)73 void Query::addType(const QString &type)
74 {
75     d->m_types << type.split(QLatin1Char('/'), Qt::SkipEmptyParts);
76 }
77 
addTypes(const QStringList & typeList)78 void Query::addTypes(const QStringList &typeList)
79 {
80     for (const QString &type : typeList) {
81         addType(type);
82     }
83 }
84 
setType(const QString & type)85 void Query::setType(const QString &type)
86 {
87     d->m_types.clear();
88     addType(type);
89 }
90 
setTypes(const QStringList & types)91 void Query::setTypes(const QStringList &types)
92 {
93     d->m_types = types;
94 }
95 
types() const96 QStringList Query::types() const
97 {
98     return d->m_types;
99 }
100 
searchString() const101 QString Query::searchString() const
102 {
103     return d->m_searchString;
104 }
105 
setSearchString(const QString & str)106 void Query::setSearchString(const QString &str)
107 {
108     d->m_searchString = str;
109 }
110 
limit() const111 uint Query::limit() const
112 {
113     return d->m_limit;
114 }
115 
setLimit(uint limit)116 void Query::setLimit(uint limit)
117 {
118     d->m_limit = limit;
119 }
120 
offset() const121 uint Query::offset() const
122 {
123     return d->m_offset;
124 }
125 
setOffset(uint offset)126 void Query::setOffset(uint offset)
127 {
128     d->m_offset = offset;
129 }
130 
setDateFilter(int year,int month,int day)131 void Query::setDateFilter(int year, int month, int day)
132 {
133     d->m_yearFilter = year;
134     d->m_monthFilter = month;
135     d->m_dayFilter = day;
136 }
137 
yearFilter() const138 int Query::yearFilter() const
139 {
140     return d->m_yearFilter;
141 }
142 
monthFilter() const143 int Query::monthFilter() const
144 {
145     return d->m_monthFilter;
146 }
147 
dayFilter() const148 int Query::dayFilter() const
149 {
150     return d->m_dayFilter;
151 }
152 
setSortingOption(Query::SortingOption option)153 void Query::setSortingOption(Query::SortingOption option)
154 {
155     d->m_sortingOption = option;
156 }
157 
sortingOption() const158 Query::SortingOption Query::sortingOption() const
159 {
160     return d->m_sortingOption;
161 }
162 
setSortingProperty(const QString & property)163 void Query::setSortingProperty(const QString &property)
164 {
165     d->m_sortingOption = SortProperty;
166     d->m_sortingProperty = property;
167 }
168 
sortingProperty() const169 QString Query::sortingProperty() const
170 {
171     return d->m_sortingProperty;
172 }
173 
addCustomOption(const QString & option,const QVariant & value)174 void Query::addCustomOption(const QString &option, const QVariant &value)
175 {
176     d->m_customOptions.insert(option, value);
177 }
178 
customOption(const QString & option) const179 QVariant Query::customOption(const QString &option) const
180 {
181     return d->m_customOptions.value(option);
182 }
183 
customOptions() const184 QVariantMap Query::customOptions() const
185 {
186     return d->m_customOptions;
187 }
188 
removeCustomOption(const QString & option)189 void Query::removeCustomOption(const QString &option)
190 {
191     d->m_customOptions.remove(option);
192 }
193 
194 Q_GLOBAL_STATIC_WITH_ARGS(SearchStore::List, s_searchStores, (SearchStore::searchStores()))
195 
exec()196 ResultIterator Query::exec()
197 {
198     // vHanda: Maybe this should default to allow searches on all search stores?
199     Q_ASSERT_X(!types().isEmpty(), "Akonadi::Search::Query::exec", "A query is being initialized without a type");
200     if (types().isEmpty()) {
201         return ResultIterator();
202     }
203 
204     SearchStore *storeMatch = nullptr;
205     for (const QSharedPointer<SearchStore> &store : std::as_const(*s_searchStores)) {
206         bool matches = true;
207         for (const QString &type : types()) {
208             if (!store->types().contains(type)) {
209                 matches = false;
210                 break;
211             }
212         }
213 
214         if (matches) {
215             storeMatch = store.data();
216             break;
217         }
218     }
219 
220     if (!storeMatch) {
221         return ResultIterator();
222     }
223 
224     int id = storeMatch->exec(*this);
225     return ResultIterator(id, storeMatch);
226 }
227 
toJSON() const228 QByteArray Query::toJSON() const
229 {
230     QVariantMap map;
231 
232     if (!d->m_types.isEmpty()) {
233         map[QStringLiteral("type")] = d->m_types;
234     }
235 
236     if (d->m_limit != defaultLimit) {
237         map[QStringLiteral("limit")] = d->m_limit;
238     }
239 
240     if (d->m_offset) {
241         map[QStringLiteral("offset")] = d->m_offset;
242     }
243 
244     if (!d->m_searchString.isEmpty()) {
245         map[QStringLiteral("searchString")] = d->m_searchString;
246     }
247 
248     if (d->m_term.isValid()) {
249         map[QStringLiteral("term")] = QVariant(d->m_term.toVariantMap());
250     }
251 
252     if (d->m_yearFilter >= 0) {
253         map[QStringLiteral("yearFilter")] = d->m_yearFilter;
254     }
255     if (d->m_monthFilter >= 0) {
256         map[QStringLiteral("monthFilter")] = d->m_monthFilter;
257     }
258     if (d->m_dayFilter >= 0) {
259         map[QStringLiteral("dayFilter")] = d->m_dayFilter;
260     }
261 
262     if (d->m_sortingOption != SortAuto) {
263         map[QStringLiteral("sortingOption")] = static_cast<int>(d->m_sortingOption);
264     }
265     if (!d->m_sortingProperty.isEmpty()) {
266         map[QStringLiteral("sortingProperty")] = d->m_sortingProperty;
267     }
268 
269     if (!d->m_customOptions.isEmpty()) {
270         map[QStringLiteral("customOptions")] = d->m_customOptions;
271     }
272 
273     QJsonObject jo = QJsonObject::fromVariantMap(map);
274     QJsonDocument jdoc;
275     jdoc.setObject(jo);
276     return jdoc.toJson();
277 }
278 
279 // static
fromJSON(const QByteArray & arr)280 Query Query::fromJSON(const QByteArray &arr)
281 {
282     QJsonDocument jdoc = QJsonDocument::fromJson(arr);
283     const QVariantMap map = jdoc.object().toVariantMap();
284 
285     Query query;
286     query.d->m_types = map[QStringLiteral("type")].toStringList();
287 
288     if (map.contains(QStringLiteral("limit"))) {
289         query.d->m_limit = map[QStringLiteral("limit")].toUInt();
290     } else {
291         query.d->m_limit = defaultLimit;
292     }
293 
294     query.d->m_offset = map[QStringLiteral("offset")].toUInt();
295     query.d->m_searchString = map[QStringLiteral("searchString")].toString();
296     query.d->m_term = Term::fromVariantMap(map[QStringLiteral("term")].toMap());
297 
298     if (map.contains(QStringLiteral("yearFilter"))) {
299         query.d->m_yearFilter = map[QStringLiteral("yearFilter")].toInt();
300     }
301     if (map.contains(QStringLiteral("monthFilter"))) {
302         query.d->m_monthFilter = map[QStringLiteral("monthFilter")].toInt();
303     }
304     if (map.contains(QStringLiteral("dayFilter"))) {
305         query.d->m_dayFilter = map[QStringLiteral("dayFilter")].toInt();
306     }
307 
308     if (map.contains(QStringLiteral("sortingOption"))) {
309         int option = map.value(QStringLiteral("sortingOption")).toInt();
310         query.d->m_sortingOption = static_cast<SortingOption>(option);
311     }
312 
313     if (map.contains(QStringLiteral("sortingProperty"))) {
314         query.d->m_sortingProperty = map.value(QStringLiteral("sortingProperty")).toString();
315     }
316 
317     if (map.contains(QStringLiteral("customOptions"))) {
318         QVariant var = map[QStringLiteral("customOptions")];
319         if (var.type() == QVariant::Map) {
320             query.d->m_customOptions = map[QStringLiteral("customOptions")].toMap();
321         } else if (var.type() == QVariant::Hash) {
322             QVariantHash hash = var.toHash();
323 
324             QHash<QString, QVariant>::const_iterator it = hash.constBegin();
325             const QHash<QString, QVariant>::const_iterator end = hash.constEnd();
326             for (; it != end; ++it) {
327                 query.d->m_customOptions.insert(it.key(), it.value());
328             }
329         }
330     }
331 
332     return query;
333 }
334 
toSearchUrl(const QString & title)335 QUrl Query::toSearchUrl(const QString &title)
336 {
337     QUrl url;
338     url.setScheme(QStringLiteral("akonadisearch"));
339 
340     QUrlQuery urlQuery;
341     urlQuery.addQueryItem(QStringLiteral("json"), QString::fromUtf8(toJSON()));
342 
343     if (!title.isEmpty()) {
344         urlQuery.addQueryItem(QStringLiteral("title"), title);
345     }
346 
347     url.setQuery(urlQuery);
348     return url;
349 }
350 
fromSearchUrl(const QUrl & url)351 Query Query::fromSearchUrl(const QUrl &url)
352 {
353     if (url.scheme() != QLatin1String("akonadisearch")) {
354         return Query();
355     }
356 
357     QUrlQuery urlQuery(url);
358     const QString jsonString = urlQuery.queryItemValue(QStringLiteral("json"), QUrl::FullyDecoded);
359     return Query::fromJSON(jsonString.toUtf8());
360 }
361 
titleFromQueryUrl(const QUrl & url)362 QString Query::titleFromQueryUrl(const QUrl &url)
363 {
364     QUrlQuery urlQuery(url);
365     return urlQuery.queryItemValue(QStringLiteral("title"), QUrl::FullyDecoded);
366 }
367 
operator ==(const Query & rhs) const368 bool Query::operator==(const Query &rhs) const
369 {
370     if (rhs.d->m_limit != d->m_limit || rhs.d->m_offset != d->m_offset || rhs.d->m_dayFilter != d->m_dayFilter || rhs.d->m_monthFilter != d->m_monthFilter
371         || rhs.d->m_yearFilter != d->m_yearFilter || rhs.d->m_customOptions != d->m_customOptions || rhs.d->m_searchString != d->m_searchString
372         || rhs.d->m_sortingProperty != d->m_sortingProperty || rhs.d->m_sortingOption != d->m_sortingOption) {
373         return false;
374     }
375 
376     if (rhs.d->m_types.size() != d->m_types.size()) {
377         return false;
378     }
379 
380     for (const QString &type : std::as_const(rhs.d->m_types)) {
381         if (!d->m_types.contains(type)) {
382             return false;
383         }
384     }
385 
386     return d->m_term == rhs.d->m_term;
387 }
388 
operator =(const Query & rhs)389 Query &Query::operator=(const Query &rhs)
390 {
391     *d = *rhs.d;
392     return *this;
393 }
394