File indexing completed on 2025-02-02 05:02:32

0001 /*
0002     SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include "passmanager.h"
0007 
0008 #include "genericpkpass.h"
0009 #include "jsonio.h"
0010 #include "logging.h"
0011 
0012 #include <KItinerary/DocumentUtil>
0013 #include <KItinerary/ExtractorPostprocessor>
0014 #include <KItinerary/ExtractorValidator>
0015 #include <KItinerary/JsonLdDocument>
0016 #include <KItinerary/MergeUtil>
0017 #include <KItinerary/ProgramMembership>
0018 #include <KItinerary/Ticket>
0019 
0020 #include <KLocalizedString>
0021 
0022 #include <QDirIterator>
0023 #include <QJsonObject>
0024 #include <QStandardPaths>
0025 #include <QUuid>
0026 
0027 using namespace KItinerary;
0028 
0029 static bool isSamePass(const GenericPkPass &lhs, const GenericPkPass &rhs)
0030 {
0031     return lhs.pkpassPassTypeIdentifier() == rhs.pkpassPassTypeIdentifier() && lhs.pkpassSerialNumber() == rhs.pkpassSerialNumber();
0032 }
0033 
0034 QString PassManager::Entry::name() const
0035 {
0036     if (JsonLd::isA<KItinerary::ProgramMembership>(data)) {
0037         return data.value<KItinerary::ProgramMembership>().programName();
0038     }
0039     if (JsonLd::isA<GenericPkPass>(data)) {
0040         return data.value<GenericPkPass>().name();
0041     }
0042     if (JsonLd::isA<KItinerary::Ticket>(data)) {
0043         return data.value<KItinerary::Ticket>().name();
0044     }
0045     return {};
0046 }
0047 
0048 QDateTime PassManager::Entry::validFrom() const
0049 {
0050     if (JsonLd::isA<KItinerary::ProgramMembership>(data)) {
0051         return data.value<KItinerary::ProgramMembership>().validFrom();
0052     }
0053     if (JsonLd::isA<KItinerary::Ticket>(data)) {
0054         return data.value<KItinerary::Ticket>().validFrom();
0055     }
0056     return {};
0057 }
0058 
0059 QDateTime PassManager::Entry::validUntil() const
0060 {
0061     if (JsonLd::isA<KItinerary::ProgramMembership>(data)) {
0062         return data.value<KItinerary::ProgramMembership>().validUntil();
0063     }
0064     if (JsonLd::isA<GenericPkPass>(data)) {
0065         return data.value<GenericPkPass>().validUntil();
0066     }
0067     if (JsonLd::isA<KItinerary::Ticket>(data)) {
0068         return data.value<KItinerary::Ticket>().validUntil();
0069     }
0070     return {};
0071 }
0072 
0073 bool PassManager::PassComparator::operator()(const PassManager::Entry &lhs, const PassManager::Entry &rhs) const
0074 {
0075     // valid before invalid, then sorted by name
0076     const auto lhsExpired = lhs.validUntil().isValid() && lhs.validUntil() < m_baseTime;
0077     const auto rhsExpired = rhs.validUntil().isValid() && rhs.validUntil() < m_baseTime;
0078 
0079     if (lhsExpired == rhsExpired) {
0080         const auto nameCmp = lhs.name().localeAwareCompare(rhs.name());
0081         if (nameCmp == 0) {
0082             return lhs.id < rhs.id;
0083         }
0084         return nameCmp < 0;
0085     }
0086 
0087     return !lhsExpired;
0088 }
0089 
0090 PassManager::PassManager(QObject *parent)
0091     : QAbstractListModel(parent)
0092     , m_baseTime(QDateTime::currentDateTime())
0093 {
0094     MergeUtil::registerComparator(isSamePass);
0095     load();
0096 }
0097 
0098 PassManager::~PassManager() = default;
0099 
0100 int PassManager::rowCount(const QModelIndex &parent) const
0101 {
0102     if (parent.isValid()) {
0103         return 0;
0104     }
0105     return m_entries.size();
0106 }
0107 
0108 QString PassManager::import(const QVariant &pass, const QString &id)
0109 {
0110     // check if this is an element we already have
0111     for (auto it = m_entries.begin(); it != m_entries.end(); ++it) {
0112         if ((!id.isEmpty() && (*it).id == id) || MergeUtil::isSame((*it).data, pass)) {
0113             (*it).data = MergeUtil::merge((*it).data, pass);
0114             write((*it).data, (*it).id);
0115             const auto idx = index(std::distance(m_entries.begin(), it), 0);
0116             Q_EMIT dataChanged(idx, idx);
0117             Q_EMIT passChanged((*it).id);
0118             return (*it).id;
0119         }
0120     }
0121 
0122     if (JsonLd::isA<KItinerary::ProgramMembership>(pass) || JsonLd::isA<GenericPkPass>(pass) || JsonLd::isA<KItinerary::Ticket>(pass)) {
0123         Entry entry;
0124         entry.id = id.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : id;
0125         entry.data = pass;
0126         if (!write(entry.data, entry.id)) {
0127             return {};
0128         }
0129 
0130         auto it = std::lower_bound(m_entries.begin(), m_entries.end(), entry, PassComparator(m_baseTime));
0131         if (it != m_entries.end() && (*it).id == entry.id) {
0132             (*it).data = entry.data;
0133             const auto idx = index(std::distance(m_entries.begin(), it), 0);
0134             Q_EMIT dataChanged(idx, idx);
0135             Q_EMIT passChanged((*it).id);
0136         } else {
0137             const auto row = std::distance(m_entries.begin(), it);
0138             beginInsertRows({}, row, row);
0139             it = m_entries.insert(it, std::move(entry));
0140             endInsertRows();
0141         }
0142         return (*it).id;
0143     }
0144 
0145     return {};
0146 }
0147 
0148 QStringList PassManager::import(const QVector<QVariant> &passes)
0149 {
0150     ExtractorPostprocessor postproc;
0151     postproc.process(passes);
0152     const auto processed = postproc.result();
0153 
0154     ExtractorValidator validator;
0155     validator.setAcceptedTypes<KItinerary::Ticket, KItinerary::ProgramMembership>();
0156 
0157     QStringList result;
0158     for (const auto &pass : processed) {
0159         if (validator.isValidElement(pass)) {
0160             auto id = import(pass);
0161             if (!id.isEmpty()) {
0162                 result.push_back(id);
0163             }
0164         }
0165     }
0166     return result;
0167 }
0168 
0169 QString PassManager::findMatchingPass(const QVariant &pass) const
0170 {
0171     if (JsonLd::isA<KItinerary::ProgramMembership>(pass)) {
0172         const auto program = pass.value<KItinerary::ProgramMembership>();
0173         if (!program.membershipNumber().isEmpty()) {
0174             const auto it = std::find_if(m_entries.begin(), m_entries.end(), [program](const auto &entry) {
0175                 return entry.data.template value<KItinerary::ProgramMembership>().membershipNumber() == program.membershipNumber();
0176             });
0177             return it == m_entries.end() ? QString() : (*it).id;
0178         }
0179         if (!program.programName().isEmpty()) {
0180             // unique substring match
0181             QString id;
0182             for (const auto &entry : m_entries) {
0183                 if (const auto dt = entry.validUntil(); dt.isValid() && dt < m_baseTime) {
0184                     continue; // expired
0185                 }
0186                 const auto name = entry.data.value<KItinerary::ProgramMembership>().programName();
0187                 if (name.isEmpty() || (!name.contains(program.programName(), Qt::CaseInsensitive) && !(program.programName().contains(name, Qt::CaseInsensitive)))) {
0188                     continue;
0189                 }
0190                 if (id.isEmpty()) {
0191                     id = entry.id;
0192                 } else {
0193                     return {};
0194                 }
0195             }
0196             return id;
0197         }
0198     }
0199     return QString();
0200 }
0201 
0202 QVariant PassManager::pass(const QString &passId) const
0203 {
0204     const auto it = std::find_if(m_entries.begin(), m_entries.end(), [passId](const auto &entry) {
0205         return entry.id == passId;
0206     });
0207     return it == m_entries.end() ? QVariant(): (*it).data;
0208 }
0209 
0210 QVariant PassManager::data(const QModelIndex &index, int role) const
0211 {
0212     if (!checkIndex(index)) {
0213         return {};
0214     }
0215 
0216     auto &entry = m_entries[index.row()];
0217     switch (role) {
0218         case PassRole:
0219             return entry.data;
0220         case PassIdRole:
0221             return entry.id;
0222         case PassTypeRole:
0223             if (JsonLd::isA<KItinerary::ProgramMembership>(entry.data)) {
0224                 return ProgramMembership;
0225             }
0226             if (JsonLd::isA<GenericPkPass>(entry.data)) {
0227                 return PkPass;
0228             }
0229             if (JsonLd::isA<KItinerary::Ticket>(entry.data)) {
0230                 return Ticket;
0231             }
0232             qCWarning(Log) << "Invalid pass type!" << entry.data;
0233             // return a valid result here, as an invalid one will completely break the delegate chooser and
0234             // nothing will be display at all. With a valid type we'll get an empty item that to the very least
0235             // allows deletion
0236             return Ticket;
0237         case PassDataRole:
0238             return JsonIO::convert(rawData(entry), JsonIO::JSON);
0239         case NameRole:
0240             return entry.name();
0241         case ValidUntilRole:
0242             return entry.validUntil();
0243         case SectionRole:
0244             if (const auto dt = entry.validUntil(); dt.isValid() && dt < m_baseTime) {
0245                 return i18nc("no longer valid tickets", "Expired");
0246             }
0247             if (const auto dt = entry.validFrom(); dt.isValid() && dt > m_baseTime) {
0248                 return i18nc("not yet valid tickets", "Future");
0249             }
0250             return i18nc("not yet expired tickets", "Valid");
0251         case ValidRangeLabelRole:
0252         {
0253             // TODO align this with the time labels in the trip groups? those handle a few more cases...
0254             const auto from = entry.validFrom();
0255             const auto to = entry.validUntil();
0256             if (!from.isValid()) {
0257                 return {};
0258             }
0259             const auto days = from.daysTo(to);
0260             if (!to.isValid() || days <= 1) {
0261                 return QLocale().toString(from.date(), QLocale::ShortFormat);
0262             }
0263             if (days >= 28 && days <= 31) {
0264                 return QLocale().toString(from, QStringLiteral("MMMM yyyy"));
0265             }
0266             if (days >= 365 && days <= 366) {
0267                 return QLocale().toString(from, QStringLiteral("yyyy"));
0268             }
0269             return i18n("%1 - %2", QLocale().toString(from.date(), QLocale::ShortFormat), QLocale().toString(to.date(), QLocale::ShortFormat));
0270         }
0271     }
0272 
0273     return {};
0274 }
0275 
0276 QHash<int, QByteArray> PassManager::roleNames() const
0277 {
0278     auto r = QAbstractListModel::roleNames();
0279     r.insert(PassRole, "pass");
0280     r.insert(PassIdRole, "passId");
0281     r.insert(PassTypeRole, "type");
0282     r.insert(NameRole, "name");
0283     r.insert(ValidUntilRole, "validUntil");
0284     r.insert(SectionRole, "section");
0285     r.insert(ValidRangeLabelRole, "validRangeLabel");
0286     return r;
0287 }
0288 
0289 void PassManager::load()
0290 {
0291     QDirIterator it(basePath(), QDir::Files);
0292     while (it.hasNext()) {
0293         it.next();
0294         Entry entry;
0295         entry.id = it.fileName();
0296         const auto data = rawData(entry);
0297         if (!data.isEmpty()) {
0298             entry.data = JsonLdDocument::fromJsonSingular(JsonIO::read(data).toObject());
0299         }
0300         m_entries.push_back(std::move(entry));
0301     }
0302     std::sort(m_entries.begin(), m_entries.end(), PassComparator(m_baseTime));
0303 }
0304 
0305 bool PassManager::write(const QVariant &data, const QString &id) const
0306 {
0307     auto path = basePath();
0308     QDir().mkpath(path);
0309     path += id;
0310     QFile f(path);
0311     if (!f.open(QFile::WriteOnly)) {
0312         qCWarning(Log) << "Failed to open file:" << f.fileName() << f.errorString();
0313         return false;
0314     }
0315     f.write(JsonIO::write(JsonLdDocument::toJson(data)));
0316     return true;
0317 }
0318 
0319 QByteArray PassManager::rawData(const Entry &entry) const
0320 {
0321     QFile f(basePath() + entry.id);
0322     if (!f.open(QFile::ReadOnly)) {
0323         qCWarning(Log) << "Failed to open file:" << f.fileName() << f.errorString();
0324         return {};
0325     }
0326 
0327     return f.readAll();
0328 }
0329 
0330 void PassManager::update(const QString &passId, const QVariant &pass)
0331 {
0332     auto it = std::find_if(m_entries.begin(), m_entries.end(), [&passId](const auto &lhs) { return lhs.id == passId; });
0333     if (it == m_entries.end()) {
0334         qWarning() << "couldn't find pass to update!?" << passId << pass;
0335         return;
0336     }
0337     (*it).data = pass;
0338     write((*it).data, (*it).id);
0339     Q_EMIT passChanged((*it).id);
0340 
0341     // the updated pass might have changed properties used for sorting here
0342     beginResetModel();
0343     std::sort(m_entries.begin(), m_entries.end(), PassComparator(m_baseTime));
0344     endResetModel();
0345 }
0346 
0347 bool PassManager::remove(const QString &passId)
0348 {
0349     auto it = std::find_if(m_entries.begin(), m_entries.end(), [passId](const auto &entry) {
0350         return entry.id == passId;
0351     });
0352     if (it != m_entries.end()) {
0353         return removeRow(std::distance(m_entries.begin(), it));
0354     }
0355     return false;
0356 }
0357 
0358 bool PassManager::removeRow(int row, const QModelIndex& parent)
0359 {
0360     return QAbstractListModel::removeRow(row, parent);
0361 }
0362 
0363 bool PassManager::removeRows(int row, int count, const QModelIndex& parent)
0364 {
0365     if (parent.isValid()) {
0366         return false;
0367     }
0368 
0369     const auto path = basePath();
0370     beginRemoveRows({}, row, row + count - 1);
0371     for (int i = row; i < row + count; ++i) {
0372         QFile::remove(path + m_entries[i].id);
0373     }
0374     m_entries.erase(m_entries.begin() + row, m_entries.begin() + row + count);
0375     endRemoveRows();
0376     return true;
0377 }
0378 
0379 QVariantList PassManager::documentIds(const QVariant& pass)
0380 {
0381     return DocumentUtil::documentIds(pass);
0382 }
0383 
0384 QString PassManager::basePath()
0385 {
0386     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/programs/");
0387 }
0388 
0389 #include "moc_passmanager.cpp"