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