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"