1 /*
2 * articlematcher.cpp
3 *
4 * SPDX-FileCopyrightText: 2004, 2005 Frerich Raabe <raabe@kde.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8 #include "articlematcher.h"
9 #include "akregator_debug.h"
10 #include "article.h"
11 #include "types.h"
12 #include <KConfig>
13 #include <KConfigGroup>
14 #include <QUrl>
15
16 #include <QRegularExpression>
17
18 namespace Akregator
19 {
20 namespace Filters
21 {
AbstractMatcher()22 AbstractMatcher::AbstractMatcher()
23 {
24 }
25
~AbstractMatcher()26 AbstractMatcher::~AbstractMatcher()
27 {
28 }
29
subjectToString(Subject subj)30 QString Criterion::subjectToString(Subject subj)
31 {
32 switch (subj) {
33 case Title:
34 return QStringLiteral("Title");
35 case Link:
36 return QStringLiteral("Link");
37 case Description:
38 return QStringLiteral("Description");
39 case Status:
40 return QStringLiteral("Status");
41 case KeepFlag:
42 return QStringLiteral("KeepFlag");
43 case Author:
44 return QStringLiteral("Author");
45 }
46 return {};
47 }
48
stringToSubject(const QString & subjStr)49 Criterion::Subject Criterion::stringToSubject(const QString &subjStr)
50 {
51 if (subjStr == QLatin1String("Title")) {
52 return Title;
53 } else if (subjStr == QLatin1String("Link")) {
54 return Link;
55 } else if (subjStr == QLatin1String("Description")) {
56 return Description;
57 } else if (subjStr == QLatin1String("Status")) {
58 return Status;
59 } else if (subjStr == QLatin1String("KeepFlag")) {
60 return KeepFlag;
61 } else if (subjStr == QLatin1String("Author")) {
62 return Author;
63 }
64
65 // hopefully never reached
66 return Description;
67 }
68
predicateToString(Predicate pred)69 QString Criterion::predicateToString(Predicate pred)
70 {
71 switch (pred) {
72 case Contains:
73 return QStringLiteral("Contains");
74 case Equals:
75 return QStringLiteral("Equals");
76 case Matches:
77 return QStringLiteral("Matches");
78 case Negation:
79 return QStringLiteral("Negation");
80 }
81 return {};
82 }
83
stringToPredicate(const QString & predStr)84 Criterion::Predicate Criterion::stringToPredicate(const QString &predStr)
85 {
86 if (predStr == QLatin1String("Contains")) {
87 return Contains;
88 } else if (predStr == QLatin1String("Equals")) {
89 return Equals;
90 } else if (predStr == QLatin1String("Matches")) {
91 return Matches;
92 } else if (predStr == QLatin1String("Negation")) {
93 return Negation;
94 }
95
96 // hopefully never reached
97 return Contains;
98 }
99
Criterion()100 Criterion::Criterion()
101 {
102 }
103
Criterion(Subject subject,Predicate predicate,const QVariant & object)104 Criterion::Criterion(Subject subject, Predicate predicate, const QVariant &object)
105 : m_subject(subject)
106 , m_predicate(predicate)
107 , m_object(object)
108 {
109 }
110
writeConfig(KConfigGroup * config) const111 void Criterion::writeConfig(KConfigGroup *config) const
112 {
113 config->writeEntry(QStringLiteral("subject"), subjectToString(m_subject));
114
115 config->writeEntry(QStringLiteral("predicate"), predicateToString(m_predicate));
116
117 config->writeEntry(QStringLiteral("objectType"), QString::fromLatin1(m_object.typeName()));
118
119 config->writeEntry(QStringLiteral("objectValue"), m_object);
120 }
121
readConfig(KConfigGroup * config)122 void Criterion::readConfig(KConfigGroup *config)
123 {
124 m_subject = stringToSubject(config->readEntry(QStringLiteral("subject"), QString()));
125 m_predicate = stringToPredicate(config->readEntry(QStringLiteral("predicate"), QString()));
126 QVariant::Type type = QVariant::nameToType(config->readEntry(QStringLiteral("objType"), QString()).toLatin1().constData());
127
128 if (type != QVariant::Invalid) {
129 m_object = config->readEntry(QStringLiteral("objectValue"), QVariant(type));
130 }
131 }
132
satisfiedBy(const Article & article) const133 bool Criterion::satisfiedBy(const Article &article) const
134 {
135 if (article.isNull()) {
136 return false;
137 }
138
139 QVariant concreteSubject;
140
141 switch (m_subject) {
142 case Title:
143 concreteSubject = QVariant(article.title());
144 break;
145 case Description:
146 concreteSubject = QVariant(article.description());
147 break;
148 case Link:
149 // ### Maybe use prettyUrl here?
150 concreteSubject = QVariant(article.link().url());
151 break;
152 case Status:
153 concreteSubject = QVariant(article.status());
154 break;
155 case KeepFlag:
156 concreteSubject = QVariant(article.keep());
157 break;
158 case Author:
159 concreteSubject = QVariant(article.authorName());
160 }
161
162 bool satisfied = false;
163
164 const auto predicateType = static_cast<Predicate>(m_predicate & ~Negation);
165 QString subjectType = QLatin1String(concreteSubject.typeName());
166
167 switch (predicateType) {
168 case Contains:
169 satisfied = concreteSubject.toString().indexOf(m_object.toString(), 0, Qt::CaseInsensitive) != -1;
170 break;
171 case Equals:
172 if (subjectType == QLatin1String("int")) {
173 satisfied = concreteSubject.toInt() == m_object.toInt();
174 } else {
175 satisfied = concreteSubject.toString() == m_object.toString();
176 }
177 break;
178 case Matches:
179 satisfied = concreteSubject.toString().contains(QRegularExpression(m_object.toString()));
180 break;
181 default:
182 qCDebug(AKREGATOR_LOG) << "Internal inconsistency; predicateType should never be Negation";
183 break;
184 }
185
186 if (m_predicate & Negation) {
187 satisfied = !satisfied;
188 }
189
190 return satisfied;
191 }
192
subject() const193 Criterion::Subject Criterion::subject() const
194 {
195 return m_subject;
196 }
197
predicate() const198 Criterion::Predicate Criterion::predicate() const
199 {
200 return m_predicate;
201 }
202
object() const203 QVariant Criterion::object() const
204 {
205 return m_object;
206 }
207
ArticleMatcher()208 ArticleMatcher::ArticleMatcher()
209 : m_association(None)
210 {
211 }
212
~ArticleMatcher()213 ArticleMatcher::~ArticleMatcher()
214 {
215 }
216
ArticleMatcher(const QVector<Criterion> & criteria,Association assoc)217 ArticleMatcher::ArticleMatcher(const QVector<Criterion> &criteria, Association assoc)
218 : m_criteria(criteria)
219 , m_association(assoc)
220 {
221 }
222
matches(const Article & a) const223 bool ArticleMatcher::matches(const Article &a) const
224 {
225 switch (m_association) {
226 case LogicalOr:
227 return anyCriterionMatches(a);
228 case LogicalAnd:
229 return allCriteriaMatch(a);
230 default:
231 break;
232 }
233 return true;
234 }
235
writeConfig(KConfigGroup * config) const236 void ArticleMatcher::writeConfig(KConfigGroup *config) const
237 {
238 config->writeEntry(QStringLiteral("matcherAssociation"), associationToString(m_association));
239
240 config->writeEntry(QStringLiteral("matcherCriteriaCount"), m_criteria.count());
241
242 QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion");
243
244 const int criteriaSize(m_criteria.size());
245 for (int index = 0; index < criteriaSize; ++index) {
246 *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(index));
247 m_criteria.at(index).writeConfig(config);
248 }
249 }
250
readConfig(KConfigGroup * config)251 void ArticleMatcher::readConfig(KConfigGroup *config)
252 {
253 m_criteria.clear();
254 m_association = stringToAssociation(config->readEntry(QStringLiteral("matcherAssociation"), QString()));
255
256 const int count = config->readEntry(QStringLiteral("matcherCriteriaCount"), 0);
257
258 const QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion");
259
260 for (int i = 0; i < count; ++i) {
261 Criterion c;
262 *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(i));
263 c.readConfig(config);
264 m_criteria.append(c);
265 }
266 }
267
operator ==(const AbstractMatcher & other) const268 bool ArticleMatcher::operator==(const AbstractMatcher &other) const
269 {
270 auto ptr = const_cast<AbstractMatcher *>(&other);
271 auto o = dynamic_cast<ArticleMatcher *>(ptr);
272 if (!o) {
273 return false;
274 } else {
275 return m_association == o->m_association && m_criteria == o->m_criteria;
276 }
277 }
278
operator !=(const AbstractMatcher & other) const279 bool ArticleMatcher::operator!=(const AbstractMatcher &other) const
280 {
281 return !(*this == other);
282 }
283
anyCriterionMatches(const Article & a) const284 bool ArticleMatcher::anyCriterionMatches(const Article &a) const
285 {
286 if (m_criteria.isEmpty()) {
287 return true;
288 }
289 const int criteriaSize(m_criteria.size());
290 for (int index = 0; index < criteriaSize; ++index) {
291 if (m_criteria.at(index).satisfiedBy(a)) {
292 return true;
293 }
294 }
295 return false;
296 }
297
allCriteriaMatch(const Article & a) const298 bool ArticleMatcher::allCriteriaMatch(const Article &a) const
299 {
300 if (m_criteria.isEmpty()) {
301 return true;
302 }
303 const int criteriaSize(m_criteria.size());
304 for (int index = 0; index < criteriaSize; ++index) {
305 if (!m_criteria.at(index).satisfiedBy(a)) {
306 return false;
307 }
308 }
309 return true;
310 }
311
stringToAssociation(const QString & assocStr)312 ArticleMatcher::Association ArticleMatcher::stringToAssociation(const QString &assocStr)
313 {
314 if (assocStr == QLatin1String("LogicalAnd")) {
315 return LogicalAnd;
316 } else if (assocStr == QLatin1String("LogicalOr")) {
317 return LogicalOr;
318 } else {
319 return None;
320 }
321 }
322
associationToString(Association association)323 QString ArticleMatcher::associationToString(Association association)
324 {
325 switch (association) {
326 case LogicalAnd:
327 return QStringLiteral("LogicalAnd");
328 case LogicalOr:
329 return QStringLiteral("LogicalOr");
330 default:
331 return QStringLiteral("None");
332 }
333 }
334 } // namespace Filters
335 } // namespace Akregator
336