File indexing completed on 2024-05-05 09:27:13
0001 /* 0002 * articlematcher.cpp 0003 * 0004 * SPDX-FileCopyrightText: 2004, 2005 Frerich Raabe <raabe@kde.org> 0005 * 0006 * SPDX-License-Identifier: BSD-2-Clause 0007 */ 0008 #include "articlematcher.h" 0009 #include "akregator_debug.h" 0010 #include "article.h" 0011 #include <KConfig> 0012 #include <KConfigGroup> 0013 #include <QUrl> 0014 #include <TextUtils/ConvertText> 0015 0016 #include <QRegularExpression> 0017 0018 namespace Akregator 0019 { 0020 namespace Filters 0021 { 0022 AbstractMatcher::AbstractMatcher() = default; 0023 0024 AbstractMatcher::~AbstractMatcher() = default; 0025 0026 QString Criterion::subjectToString(Subject subj) 0027 { 0028 switch (subj) { 0029 case Title: 0030 return QStringLiteral("Title"); 0031 case Link: 0032 return QStringLiteral("Link"); 0033 case Description: 0034 return QStringLiteral("Description"); 0035 case Status: 0036 return QStringLiteral("Status"); 0037 case KeepFlag: 0038 return QStringLiteral("KeepFlag"); 0039 case Author: 0040 return QStringLiteral("Author"); 0041 } 0042 return {}; 0043 } 0044 0045 Criterion::Subject Criterion::stringToSubject(const QString &subjStr) 0046 { 0047 if (subjStr == QLatin1String("Title")) { 0048 return Title; 0049 } else if (subjStr == QLatin1String("Link")) { 0050 return Link; 0051 } else if (subjStr == QLatin1String("Description")) { 0052 return Description; 0053 } else if (subjStr == QLatin1String("Status")) { 0054 return Status; 0055 } else if (subjStr == QLatin1String("KeepFlag")) { 0056 return KeepFlag; 0057 } else if (subjStr == QLatin1String("Author")) { 0058 return Author; 0059 } 0060 0061 // hopefully never reached 0062 return Description; 0063 } 0064 0065 QString Criterion::predicateToString(Predicate pred) 0066 { 0067 switch (pred) { 0068 case Contains: 0069 return QStringLiteral("Contains"); 0070 case Equals: 0071 return QStringLiteral("Equals"); 0072 case Matches: 0073 return QStringLiteral("Matches"); 0074 case Negation: 0075 return QStringLiteral("Negation"); 0076 } 0077 return {}; 0078 } 0079 0080 Criterion::Predicate Criterion::stringToPredicate(const QString &predStr) 0081 { 0082 if (predStr == QLatin1String("Contains")) { 0083 return Contains; 0084 } else if (predStr == QLatin1String("Equals")) { 0085 return Equals; 0086 } else if (predStr == QLatin1String("Matches")) { 0087 return Matches; 0088 } else if (predStr == QLatin1String("Negation")) { 0089 return Negation; 0090 } 0091 0092 // hopefully never reached 0093 return Contains; 0094 } 0095 0096 Criterion::Criterion() = default; 0097 0098 Criterion::Criterion(Subject subject, Predicate predicate, const QVariant &object) 0099 : m_subject(subject) 0100 , m_predicate(predicate) 0101 , m_object(object) 0102 { 0103 } 0104 0105 void Criterion::writeConfig(KConfigGroup *config) const 0106 { 0107 config->writeEntry(QStringLiteral("subject"), subjectToString(m_subject)); 0108 0109 config->writeEntry(QStringLiteral("predicate"), predicateToString(m_predicate)); 0110 0111 config->writeEntry(QStringLiteral("objectType"), QString::fromLatin1(m_object.typeName())); 0112 0113 config->writeEntry(QStringLiteral("objectValue"), m_object); 0114 } 0115 0116 void Criterion::readConfig(KConfigGroup *config) 0117 { 0118 m_subject = stringToSubject(config->readEntry(QStringLiteral("subject"), QString())); 0119 m_predicate = stringToPredicate(config->readEntry(QStringLiteral("predicate"), QString())); 0120 QMetaType type = QMetaType::fromName(config->readEntry(QStringLiteral("objType"), QString()).toLatin1().constData()); 0121 0122 if (QMetaType::Type(type.id()) != QMetaType::UnknownType) { 0123 m_object = config->readEntry(QStringLiteral("objectValue"), QVariant(type)); 0124 } 0125 } 0126 0127 bool Criterion::satisfiedBy(const Article &article) const 0128 { 0129 if (article.isNull()) { 0130 return false; 0131 } 0132 0133 QVariant concreteSubject; 0134 0135 switch (m_subject) { 0136 case Title: 0137 concreteSubject = QVariant(article.title()); 0138 break; 0139 case Description: 0140 concreteSubject = QVariant(article.description()); 0141 break; 0142 case Link: 0143 // ### Maybe use prettyUrl here? 0144 concreteSubject = QVariant(article.link().url()); 0145 break; 0146 case Status: 0147 concreteSubject = QVariant(article.status()); 0148 break; 0149 case KeepFlag: 0150 concreteSubject = QVariant(article.keep()); 0151 break; 0152 case Author: 0153 concreteSubject = QVariant(article.authorName()); 0154 } 0155 0156 bool satisfied = false; 0157 0158 const auto predicateType = static_cast<Predicate>(m_predicate & ~Negation); 0159 QString subjectType = QLatin1String(concreteSubject.typeName()); 0160 0161 switch (predicateType) { 0162 case Contains: 0163 satisfied = TextUtils::ConvertText::normalize(concreteSubject.toString()).indexOf(m_object.toString(), 0, Qt::CaseInsensitive) != -1; 0164 break; 0165 case Equals: 0166 if (subjectType == QLatin1String("int")) { 0167 satisfied = concreteSubject.toInt() == m_object.toInt(); 0168 } else { 0169 satisfied = TextUtils::ConvertText::normalize(concreteSubject.toString()) == m_object.toString(); 0170 } 0171 break; 0172 case Matches: 0173 satisfied = TextUtils::ConvertText::normalize(concreteSubject.toString()).contains(QRegularExpression(m_object.toString())); 0174 break; 0175 default: 0176 qCDebug(AKREGATOR_LOG) << "Internal inconsistency; predicateType should never be Negation"; 0177 break; 0178 } 0179 0180 if (m_predicate & Negation) { 0181 satisfied = !satisfied; 0182 } 0183 0184 return satisfied; 0185 } 0186 0187 Criterion::Subject Criterion::subject() const 0188 { 0189 return m_subject; 0190 } 0191 0192 Criterion::Predicate Criterion::predicate() const 0193 { 0194 return m_predicate; 0195 } 0196 0197 QVariant Criterion::object() const 0198 { 0199 return m_object; 0200 } 0201 0202 ArticleMatcher::ArticleMatcher() 0203 : m_association(None) 0204 { 0205 } 0206 0207 ArticleMatcher::~ArticleMatcher() = default; 0208 0209 ArticleMatcher::ArticleMatcher(const QList<Criterion> &criteria, Association assoc) 0210 : m_criteria(criteria) 0211 , m_association(assoc) 0212 { 0213 } 0214 0215 bool ArticleMatcher::matches(const Article &a) const 0216 { 0217 switch (m_association) { 0218 case LogicalOr: 0219 return anyCriterionMatches(a); 0220 case LogicalAnd: 0221 return allCriteriaMatch(a); 0222 default: 0223 break; 0224 } 0225 return true; 0226 } 0227 0228 void ArticleMatcher::writeConfig(KConfigGroup *config) const 0229 { 0230 config->writeEntry(QStringLiteral("matcherAssociation"), associationToString(m_association)); 0231 0232 config->writeEntry(QStringLiteral("matcherCriteriaCount"), m_criteria.count()); 0233 0234 QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion"); 0235 0236 const int criteriaSize(m_criteria.size()); 0237 for (int index = 0; index < criteriaSize; ++index) { 0238 *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(index)); 0239 m_criteria.at(index).writeConfig(config); 0240 } 0241 } 0242 0243 void ArticleMatcher::readConfig(KConfigGroup *config) 0244 { 0245 m_criteria.clear(); 0246 m_association = stringToAssociation(config->readEntry(QStringLiteral("matcherAssociation"), QString())); 0247 0248 const int count = config->readEntry(QStringLiteral("matcherCriteriaCount"), 0); 0249 0250 const QString criterionGroupPrefix = config->name() + QLatin1String("_Criterion"); 0251 0252 for (int i = 0; i < count; ++i) { 0253 Criterion c; 0254 *config = KConfigGroup(config->config(), criterionGroupPrefix + QString::number(i)); 0255 c.readConfig(config); 0256 m_criteria.append(c); 0257 } 0258 } 0259 0260 bool ArticleMatcher::operator==(const AbstractMatcher &other) const 0261 { 0262 auto ptr = const_cast<AbstractMatcher *>(&other); 0263 auto o = dynamic_cast<ArticleMatcher *>(ptr); 0264 if (!o) { 0265 return false; 0266 } else { 0267 return m_association == o->m_association && m_criteria == o->m_criteria; 0268 } 0269 } 0270 0271 bool ArticleMatcher::operator!=(const AbstractMatcher &other) const 0272 { 0273 return !(*this == other); 0274 } 0275 0276 bool ArticleMatcher::anyCriterionMatches(const Article &a) const 0277 { 0278 if (m_criteria.isEmpty()) { 0279 return true; 0280 } 0281 const int criteriaSize(m_criteria.size()); 0282 for (int index = 0; index < criteriaSize; ++index) { 0283 if (m_criteria.at(index).satisfiedBy(a)) { 0284 return true; 0285 } 0286 } 0287 return false; 0288 } 0289 0290 bool ArticleMatcher::allCriteriaMatch(const Article &a) const 0291 { 0292 if (m_criteria.isEmpty()) { 0293 return true; 0294 } 0295 const int criteriaSize(m_criteria.size()); 0296 for (int index = 0; index < criteriaSize; ++index) { 0297 if (!m_criteria.at(index).satisfiedBy(a)) { 0298 return false; 0299 } 0300 } 0301 return true; 0302 } 0303 0304 ArticleMatcher::Association ArticleMatcher::stringToAssociation(const QString &assocStr) 0305 { 0306 if (assocStr == QLatin1String("LogicalAnd")) { 0307 return LogicalAnd; 0308 } else if (assocStr == QLatin1String("LogicalOr")) { 0309 return LogicalOr; 0310 } else { 0311 return None; 0312 } 0313 } 0314 0315 QString ArticleMatcher::associationToString(Association association) 0316 { 0317 switch (association) { 0318 case LogicalAnd: 0319 return QStringLiteral("LogicalAnd"); 0320 case LogicalOr: 0321 return QStringLiteral("LogicalOr"); 0322 default: 0323 return QStringLiteral("None"); 0324 } 0325 } 0326 } // namespace Filters 0327 } // namespace Akregator