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"