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

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "reservationmanager.h"
0008 
0009 #include "jsonio.h"
0010 #include "logging.h"
0011 #include "reservationhelper.h"
0012 
0013 #include <KItinerary/ExtractorPostprocessor>
0014 #include <KItinerary/Event>
0015 #include <KItinerary/Flight>
0016 #include <KItinerary/JsonLdDocument>
0017 #include <KItinerary/MergeUtil>
0018 #include <KItinerary/Reservation>
0019 #include <KItinerary/SortUtil>
0020 #include <KItinerary/Visit>
0021 
0022 #include <KLocalizedString>
0023 
0024 #include <QDate>
0025 #include <QDir>
0026 #include <QDirIterator>
0027 #include <QFile>
0028 #include <QJsonArray>
0029 #include <QJsonObject>
0030 #include <QScopeGuard>
0031 #include <QStandardPaths>
0032 #include <QUrl>
0033 #include <QUuid>
0034 #include <QVector>
0035 
0036 using namespace KItinerary;
0037 
0038 ReservationManager::ReservationManager(QObject* parent)
0039     : QObject(parent)
0040 {
0041     m_validator.setAcceptedTypes<
0042         BoatReservation,
0043         BusReservation,
0044         EventReservation,
0045         FlightReservation,
0046         FoodEstablishmentReservation,
0047         LodgingReservation,
0048         RentalCarReservation,
0049         TrainReservation,
0050         TouristAttractionVisit
0051     >();
0052     m_validator.setAcceptOnlyCompleteElements(true);
0053 
0054     loadBatches();
0055 }
0056 
0057 ReservationManager::~ReservationManager() = default;
0058 
0059 bool ReservationManager::isEmpty() const
0060 {
0061     return m_batchToResMap.empty();
0062 }
0063 
0064 bool ReservationManager::hasBatch(const QString &batchId) const
0065 {
0066     return m_batchToResMap.contains(batchId);
0067 }
0068 
0069 QVariant ReservationManager::reservation(const QString& id) const
0070 {
0071     if (id.isEmpty()) {
0072         return {};
0073     }
0074 
0075     const auto it = m_reservations.constFind(id);
0076     if (it != m_reservations.constEnd()) {
0077         return it.value();
0078     }
0079 
0080     const QString resPath = reservationsBasePath() + id + QLatin1StringView(".jsonld");
0081     QFile f(resPath);
0082     if (!f.open(QFile::ReadOnly)) {
0083         qCWarning(Log) << "Failed to open JSON-LD reservation data file:" << resPath << f.errorString();
0084         return {};
0085     }
0086 
0087     const auto val = JsonIO::read(f.readAll());
0088     if (!(val.isArray() && val.toArray().size() == 1) && !val.isObject()) {
0089         qCWarning(Log) << "Invalid JSON-LD reservation data file:" << resPath;
0090         return {};
0091     }
0092 
0093     const auto resData = JsonLdDocument::fromJson(val.isArray() ? val.toArray() : QJsonArray({val.toObject()}));
0094     if (resData.size() != 1) {
0095         qCWarning(Log) << "Unable to parse JSON-LD reservation data file:" << resPath;
0096         return {};
0097     }
0098 
0099     // re-run post-processing to benefit from newer augmentations
0100     ExtractorPostprocessor postproc;
0101     postproc.process(resData);
0102     if (postproc.result().size() != 1) {
0103         qCWarning(Log) << "Post-processing discarded the reservation:" << resPath;
0104         return {};
0105     }
0106 
0107     const auto res = postproc.result().at(0);
0108     if (!m_validator.isValidElement(res)) {
0109         qCWarning(Log) << "Validation discarded the reservation:" << resPath;
0110         return {};
0111     }
0112     m_reservations.insert(id, res);
0113     return res;
0114 }
0115 
0116 QString ReservationManager::reservationsBasePath()
0117 {
0118     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/reservations/");
0119 }
0120 
0121 QString ReservationManager::batchesBasePath()
0122 {
0123     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1StringView("/batches/");
0124 }
0125 
0126 QVector<QString> ReservationManager::importReservations(const QVector<QVariant> &resData)
0127 {
0128     ExtractorPostprocessor postproc;
0129     postproc.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0)));
0130     postproc.process(resData);
0131 
0132     auto data = postproc.result();
0133     QVector<QString> ids;
0134     ids.reserve(data.size());
0135     for (auto &res : data) {
0136         if (JsonLd::isA<Event>(res)) { // promote Event to EventReservation
0137             EventReservation ev;
0138             ev.setReservationFor(res);
0139             ev.setPotentialAction(res.value<Event>().potentialAction());
0140             res = ev;
0141         }
0142         // TODO show UI asking for time ranges for LodgingBusiness, FoodEstablishment, etc
0143 
0144         // filter out non-Reservation objects we can't handle yet
0145         if (!m_validator.isValidElement(res)) {
0146 
0147             // check if this is a minimal cancellation element
0148             const auto cleanup = qScopeGuard([this]{ m_validator.setAcceptOnlyCompleteElements(true); });
0149             m_validator.setAcceptOnlyCompleteElements(false);
0150             if (m_validator.isValidElement(res)) {
0151                 ids += applyPartialUpdate(res);
0152                 continue;
0153             }
0154 
0155             qCWarning(Log) << "Discarding imported element due to validation failure" << res;
0156             continue;
0157         }
0158 
0159         ids.push_back(addReservation(res));
0160     }
0161 
0162     return ids;
0163 }
0164 
0165 QString ReservationManager::addReservation(const QVariant &res, const QString &resIdHint)
0166 {
0167     // look for matching reservations, or matching batches
0168     // we need to do that within a +/-24h range, so we find unbound elements too
0169     // TODO in case this updates the time for an unbound element we need to re-sort, otherwise the prev/next logic fails!
0170     const auto rangeBegin = SortUtil::startDateTime(res).addDays(-1);
0171     const auto rangeEnd = rangeBegin.addDays(2);
0172 
0173     const auto beginIt = std::lower_bound(m_batches.begin(), m_batches.end(), rangeBegin, [this](const auto &lhs, const auto &rhs) {
0174         return SortUtil::startDateTime(reservation(lhs)) < rhs;
0175     });
0176     for (auto it = beginIt; it != m_batches.end(); ++it) {
0177         const auto otherRes = reservation(*it);
0178         if (SortUtil::startDateTime(otherRes) > rangeEnd) {
0179             break; // no hit
0180         }
0181         if (MergeUtil::isSame(res, otherRes)) {
0182             // this is actually an update of otherRes!
0183             const auto newRes = MergeUtil::merge(otherRes, res);
0184             updateReservation(*it, newRes);
0185             return *it;
0186         }
0187         if (MergeUtil::isSameIncidence(res, otherRes)) {
0188             // this is a multi-traveler element, check if we have it as one of the batch elements already
0189             const auto &batch = m_batchToResMap.value(*it);
0190             for (const auto &batchedId : batch) {
0191                 const auto batchedRes = reservation(batchedId);
0192                 if (MergeUtil::isSame(res, batchedRes)) {
0193                     // this is actually an update of a batched reservation
0194                     const auto newRes = MergeUtil::merge(otherRes, res);
0195                     updateReservation(batchedId, newRes);
0196                     return batchedId;
0197                 }
0198             }
0199 
0200             // truly new, and added to an existing batch
0201             const QString resId = makeReservationId(resIdHint);
0202             storeReservation(resId, res);
0203             Q_EMIT reservationAdded(resId);
0204 
0205             m_batchToResMap[*it].push_back(resId);
0206             m_resToBatchMap.insert(resId, *it);
0207             Q_EMIT batchChanged(*it);
0208             storeBatch(*it);
0209             return resId;
0210         }
0211     }
0212 
0213     // truly new, and starting a new batch
0214     const QString resId = makeReservationId(resIdHint);
0215     storeReservation(resId, res);
0216     Q_EMIT reservationAdded(resId);
0217 
0218     // search for the precise insertion place, beginIt is only the begin of our initial search range
0219     const auto insertIt = std::lower_bound(m_batches.begin(), m_batches.end(), SortUtil::startDateTime(res), [this](const auto &lhs, const auto &rhs) {
0220         return SortUtil::startDateTime(reservation(lhs)) < rhs;
0221     });
0222     m_batches.insert(insertIt, resId);
0223     m_batchToResMap.insert(resId, {resId});
0224     m_resToBatchMap.insert(resId, resId);
0225     Q_EMIT batchAdded(resId);
0226     storeBatch(resId);
0227     return resId;
0228 }
0229 
0230 void ReservationManager::importReservation(const QVariant &resData)
0231 {
0232     importReservations({resData});
0233 }
0234 
0235 void ReservationManager::updateReservation(const QString &resId, const QVariant &res)
0236 {
0237     const auto oldRes = reservation(resId);
0238 
0239     storeReservation(resId, res);
0240     Q_EMIT reservationChanged(resId);
0241 
0242     updateBatch(resId, res, oldRes);
0243 }
0244 
0245 void ReservationManager::storeReservation(const QString &resId, const QVariant &res) const
0246 {
0247     const QString basePath = reservationsBasePath();
0248     QDir::root().mkpath(basePath);
0249     const QString path = basePath + resId + QLatin1StringView(".jsonld");
0250     QFile f(path);
0251     if (!f.open(QFile::WriteOnly)) {
0252         qCWarning(Log) << "Unable to open file:" << f.errorString();
0253         return;
0254     }
0255     f.write(JsonIO::write(JsonLdDocument::toJson(res)));
0256     m_reservations.insert(resId, res);
0257 }
0258 
0259 void ReservationManager::removeReservation(const QString& id)
0260 {
0261     const auto batchId = m_resToBatchMap.value(id);
0262     removeFromBatch(id, batchId);
0263 
0264     const QString basePath = reservationsBasePath();
0265     QFile::remove(basePath + QLatin1Char('/') + id + QLatin1StringView(".jsonld"));
0266     Q_EMIT reservationRemoved(id);
0267     m_reservations.remove(id);
0268 }
0269 
0270 const std::vector<QString>& ReservationManager::batches() const
0271 {
0272     return m_batches;
0273 }
0274 
0275 QString ReservationManager::batchForReservation(const QString &resId) const
0276 {
0277     return m_resToBatchMap.value(resId);
0278 }
0279 
0280 QStringList ReservationManager::reservationsForBatch(const QString &batchId) const
0281 {
0282     return m_batchToResMap.value(batchId);
0283 }
0284 
0285 void ReservationManager::removeBatch(const QString &batchId)
0286 {
0287     // TODO make this atomic, ie. don't emit batch range notifications
0288     const auto res = m_batchToResMap.value(batchId);
0289     for (const auto &id : res) {
0290         if (id != batchId) {
0291             removeReservation(id);
0292         }
0293     }
0294     removeReservation(batchId);
0295 }
0296 
0297 void ReservationManager::loadBatches()
0298 {
0299     Q_ASSERT(m_batches.empty());
0300 
0301     const auto base = batchesBasePath();
0302     if (!QDir::root().exists(base)) {
0303         initialBatchCreate();
0304         return;
0305     }
0306 
0307     for (QDirIterator it(base, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) {
0308         it.next();
0309         QFile batchFile(it.filePath());
0310         if (!batchFile.open(QFile::ReadOnly)) {
0311             qCWarning(Log) << "Failed to open batch file" << it.filePath() << batchFile.errorString();
0312             continue;
0313         }
0314 
0315         const auto batchId = it.fileInfo().baseName();
0316         m_batches.push_back(batchId);
0317 
0318         const auto batchVal = JsonIO::read(batchFile.readAll());
0319         const auto top = batchVal.toObject();
0320         const auto resArray = top.value(QLatin1StringView("elements")).toArray();
0321         QStringList l;
0322         l.reserve(resArray.size());
0323         for (const auto &v : resArray) {
0324             const auto resId = v.toString();
0325             l.push_back(resId);
0326             m_resToBatchMap.insert(resId, batchId);
0327         }
0328         m_batchToResMap.insert(batchId, l);
0329     }
0330 
0331     std::sort(m_batches.begin(), m_batches.end(), [this](const auto &lhs, const auto &rhs) {
0332         return SortUtil::isBefore(reservation(lhs), reservation(rhs));
0333     });
0334 }
0335 
0336 void ReservationManager::storeBatch(const QString &batchId) const
0337 {
0338     QJsonArray elems;
0339     const auto &batch = m_batchToResMap.value(batchId);
0340     std::copy(batch.begin(), batch.end(), std::back_inserter(elems));
0341 
0342     QJsonObject top;
0343     top.insert(QLatin1StringView("elements"), elems);
0344 
0345     const QString path = batchesBasePath() + batchId + QLatin1StringView(".json");
0346     QFile f(path);
0347     if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
0348         qCWarning(Log) << "Failed to open batch file!" << path << f.errorString();
0349         return;
0350     }
0351 
0352     f.write(JsonIO::write(top));
0353 }
0354 
0355 void ReservationManager::storeRemoveBatch(const QString &batchId) const
0356 {
0357     const QString path = batchesBasePath() + batchId + QLatin1StringView(".json");
0358     QFile::remove(path);
0359 }
0360 
0361 void ReservationManager::initialBatchCreate()
0362 {
0363     const auto batchBase = batchesBasePath();
0364     QDir::root().mkpath(batchBase);
0365     qCDebug(Log) << batchBase;
0366 
0367     const QSignalBlocker blocker(this);
0368     const auto base = reservationsBasePath();
0369     for (QDirIterator it(base, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) {
0370         it.next();
0371         const auto resId = it.fileInfo().baseName();
0372         const auto res = reservation(resId);
0373         updateBatch(resId, res, res);
0374     }
0375 }
0376 
0377 void ReservationManager::updateBatch(const QString &resId, const QVariant &newRes, const QVariant &oldRes)
0378 {
0379     const auto oldBatchId = batchForReservation(resId);
0380     QString newBatchId;
0381 
0382     // find the destination batch
0383     const auto beginIt = std::lower_bound(m_batches.begin(), m_batches.end(), newRes, [this](const auto &lhs, const auto &rhs) {
0384         return SortUtil::startDateTime(reservation(lhs)) < SortUtil::startDateTime(rhs);
0385     });
0386     for (auto it = beginIt; it != m_batches.end(); ++it) {
0387         const auto otherRes = (resId == (*it)) ? oldRes : reservation(*it);
0388         if (SortUtil::startDateTime(otherRes) != SortUtil::startDateTime(newRes)) {
0389             break; // no hit
0390         }
0391         if (MergeUtil::isSameIncidence(newRes, otherRes)) {
0392             newBatchId = *it;
0393             break;
0394         }
0395     }
0396 
0397     // still in the same batch?
0398     if (!oldBatchId.isEmpty() && oldBatchId == newBatchId) {
0399         Q_EMIT batchContentChanged(oldBatchId);
0400         // no need to store here, as batching didn't actually change
0401         return;
0402     }
0403 
0404     // move us out of the old batch
0405     // WARNING: beginIt will become invalid after this!
0406     removeFromBatch(resId, oldBatchId);
0407 
0408     // insert us into the new batch
0409     if (newBatchId.isEmpty()) {
0410         // we are starting a new batch
0411         // re-run search for insertion point, could be invalid due to the above deletions
0412         const auto it = std::lower_bound(m_batches.begin(), m_batches.end(), newRes, [this](const auto &lhs, const auto &rhs) {
0413             return SortUtil::startDateTime(reservation(lhs)) < SortUtil::startDateTime(rhs);
0414         });
0415         m_batches.insert(it, QString(resId));
0416         m_batchToResMap.insert(resId, {resId});
0417         m_resToBatchMap.insert(resId, resId);
0418         Q_EMIT batchAdded(resId);
0419         storeBatch(resId);
0420     } else {
0421         m_batchToResMap[newBatchId].push_back(resId);
0422         m_resToBatchMap.insert(resId, newBatchId);
0423         Q_EMIT batchChanged(newBatchId);
0424         storeBatch(newBatchId);
0425     }
0426 }
0427 
0428 void ReservationManager::removeFromBatch(const QString &resId, const QString &batchId)
0429 {
0430     if (batchId.isEmpty()) {
0431         return;
0432     }
0433 
0434     auto &batches = m_batchToResMap[batchId];
0435     m_resToBatchMap.remove(resId);
0436     if (batches.size() == 1) { // we were alone there, remove old batch
0437         m_batchToResMap.remove(batchId);
0438         const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
0439         m_batches.erase(it);
0440         Q_EMIT batchRemoved(batchId);
0441         storeRemoveBatch(batchId);
0442     } else if (resId == batchId) {
0443         // our id was the batch id, so rename the old batch
0444         batches.removeAll(resId);
0445         const QString renamedBatchId = batches.first();
0446         auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
0447         Q_ASSERT(it != m_batches.end());
0448         *it = renamedBatchId;
0449         for (const auto &id : batches) {
0450             m_resToBatchMap[id] = renamedBatchId;
0451         }
0452         m_batchToResMap[renamedBatchId] = batches;
0453         m_batchToResMap.remove(batchId);
0454         Q_EMIT batchRenamed(batchId, renamedBatchId);
0455         storeRemoveBatch(batchId);
0456         storeBatch(renamedBatchId);
0457     } else {
0458         // old batch remains
0459         batches.removeAll(resId);
0460         Q_EMIT batchChanged(batchId);
0461         storeBatch(batchId);
0462     }
0463 }
0464 
0465 QVector<QString> ReservationManager::applyPartialUpdate(const QVariant &res)
0466 {
0467     // validate input
0468     if (!JsonLd::canConvert<Reservation>(res)) {
0469         return {};
0470     }
0471     const auto baseRes = JsonLd::convert<Reservation>(res);
0472     if (!baseRes.modifiedTime().isValid()) {
0473         return {};
0474     }
0475 
0476     // look for matching reservations in a 6 month window following the modification time
0477     const auto rangeBegin = baseRes.modifiedTime();
0478     const auto rangeEnd = rangeBegin.addDays(6 * 30);
0479 
0480     QVector<QString> updatedIds;
0481     const auto beginIt = std::lower_bound(m_batches.begin(), m_batches.end(), rangeBegin, [this](const auto &lhs, const auto &rhs) {
0482         return SortUtil::startDateTime(reservation(lhs)) < rhs;
0483     });
0484     for (auto it = beginIt; it != m_batches.end(); ++it) {
0485         const auto otherRes = reservation(*it);
0486         if (SortUtil::startDateTime(otherRes) > rangeEnd) {
0487             break; // no hit
0488         }
0489         if (MergeUtil::isSame(res, otherRes)) {
0490             // this is actually an update of otherRes!
0491             const auto newRes = MergeUtil::merge(otherRes, res);
0492             updateReservation(*it, newRes);
0493             updatedIds.push_back(*it);
0494             continue;
0495         }
0496         if (MergeUtil::isSameIncidence(res, otherRes)) {
0497             // this is a multi-traveler element, check if we have it as one of the batch elements already
0498             const auto &batch = m_batchToResMap.value(*it);
0499             for (const auto &batchedId : batch) {
0500                 const auto batchedRes = reservation(batchedId);
0501                 if (MergeUtil::isSame(res, batchedRes)) {
0502                     // this is actually an update of a batched reservation
0503                     const auto newRes = MergeUtil::merge(otherRes, res);
0504                     updateReservation(batchedId, newRes);
0505                     updatedIds.push_back(batchedId);
0506                     break;
0507                 }
0508             }
0509         }
0510     }
0511 
0512     return updatedIds;
0513 }
0514 
0515 QString ReservationManager::previousBatch(const QString &batchId) const
0516 {
0517     // ### this can be optimized by relying on m_batches being sorted by start date
0518     const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
0519     if (it == m_batches.end() || it == m_batches.begin()) {
0520         return {};
0521     }
0522     return *(it - 1);
0523 }
0524 
0525 QString ReservationManager::nextBatch(const QString& batchId) const
0526 {
0527     // ### this can be optimized by relying on m_batches being sorted by start date
0528     const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
0529     if (it == m_batches.end() || m_batches.size() < 2 || it == (m_batches.end() - 1)) {
0530         return {};
0531     }
0532     return *(it + 1);
0533 }
0534 
0535 QString ReservationManager::makeReservationId(const QString &resIdHint) const
0536 {
0537     if (!resIdHint.isEmpty() && reservation(resIdHint).isNull()) {
0538         return resIdHint;
0539     }
0540     return QUuid::createUuid().toString(QUuid::WithoutBraces);
0541 }
0542 
0543 #include "moc_reservationmanager.cpp"