diff --git a/autotests/jsonlddata/multi-type.in.json b/autotests/jsonlddata/multi-type.in.json new file mode 100644 index 0000000..ca6bd84 --- /dev/null +++ b/autotests/jsonlddata/multi-type.in.json @@ -0,0 +1,8 @@ +[ + { + "@context": "http://schema.org", + "@type": [ "Hotel", "Hostel" ], + "name": "A place to stay", + "description": "Test multi-type expansion in import filtering" + } +] diff --git a/autotests/jsonlddata/multi-type.out.json b/autotests/jsonlddata/multi-type.out.json new file mode 100644 index 0000000..e941413 --- /dev/null +++ b/autotests/jsonlddata/multi-type.out.json @@ -0,0 +1,8 @@ +[ + { + "@context": "http://schema.org", + "@type": "LodgingBusiness", + "description": "Test multi-type expansion in import filtering", + "name": "A place to stay" + } +] diff --git a/src/file.cpp b/src/file.cpp index ac66f84..3b92ca3 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -1,378 +1,378 @@ /* Copyright (C) 2019 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "file.h" #include "jsonlddocument.h" #include "logging.h" #include #include #include #include #include #include #include #include #include using namespace KItinerary; namespace KItinerary { class FilePrivate { public: QString fileName; QIODevice *device = nullptr; std::unique_ptr zipFile; }; } File::File() : d(new FilePrivate) { } File::File(const QString &fileName) : d(new FilePrivate) { d->fileName = fileName; } File::File(QIODevice* device) : d(new FilePrivate) { d->device = device; } File::File(KItinerary::File &&) = default; File::~File() { close(); } File& KItinerary::File::operator=(KItinerary::File &&) = default; void File::setFileName(const QString &fileName) { d->fileName = fileName; } bool File::open(File::OpenMode mode) const { if (d->device) { d->zipFile.reset(new KZip(d->device)); } else { d->zipFile.reset(new KZip(d->fileName)); } if (!d->zipFile->open(mode == File::Write ? QIODevice::WriteOnly : QIODevice::ReadOnly)) { qCWarning(Log) << d->zipFile->errorString() << d->fileName; return false; } return true; } QString File::errorString() const { if (d->zipFile && !d->zipFile->isOpen()) { return d->zipFile->errorString(); } return {}; } void File::close() { if (d->zipFile) { d->zipFile->close(); } d->zipFile.reset(); } QVector File::reservations() const { Q_ASSERT(d->zipFile); const auto resDir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("reservations"))); if (!resDir) { return {}; } const auto entries = resDir->entries(); QVector res; res.reserve(entries.size()); for (const auto &entry : entries) { if (!entry.endsWith(QLatin1String(".json"))) { continue; } res.push_back(entry.left(entry.size() - 5)); } return res; } QVariant File::reservation(const QString &resId) const { Q_ASSERT(d->zipFile); const auto resDir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("reservations"))); if (!resDir) { return {}; } const auto file = resDir->file(resId + QLatin1String(".json")); if (!file) { qCDebug(Log) << "reservation not found" << resId; return {}; } const auto doc = QJsonDocument::fromJson(file->data()); if (doc.isArray()) { const auto array = JsonLdDocument::fromJson(doc.array()); if (array.size() != 1) { qCWarning(Log) << "reservation file for" << resId << "contains" << array.size() << "elements!"; return {}; } return array.at(0); } else if (doc.isObject()) { - return JsonLdDocument::fromJson(doc.object()); + return JsonLdDocument::fromJsonSingular(doc.object()); } return {}; } void File::addReservation(const QVariant &res) { addReservation(QUuid::createUuid().toString(), res); } void File::addReservation(const QString &id, const QVariant &res) { Q_ASSERT(d->zipFile); d->zipFile->writeFile(QLatin1String("reservations/") + id + QLatin1String(".json"), QJsonDocument(JsonLdDocument::toJson(res)).toJson()); } QString File::passId(const KPkPass::Pass *pass) { return passId(pass->passTypeIdentifier(), pass->serialNumber()); } QString File::passId(const QString &passTypeIdenfier, const QString &serialNumber) { if (passTypeIdenfier.isEmpty() || serialNumber.isEmpty()) { return {}; } // serialNumber can contain percent-encoding or slashes, ie stuff we don't want to have in file names return passTypeIdenfier + QLatin1Char('/') + QString::fromUtf8(serialNumber.toUtf8().toBase64(QByteArray::Base64UrlEncoding)); } QVector File::passes() const { Q_ASSERT(d->zipFile); const auto passDir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("passes"))); if (!passDir) { return {}; } const auto entries = passDir->entries(); QVector passIds; for (const auto &entry : entries) { const auto subdir = dynamic_cast(passDir->entry(entry)); if (!subdir) { continue; } const auto subEntries = subdir->entries(); for (const auto &subEntry : subEntries) { if (!subEntry.endsWith(QLatin1String(".pkpass"))) { continue; } passIds.push_back(entry + QLatin1Char('/') + subEntry.leftRef(subEntry.size() - 7)); } } return passIds; } QByteArray File::passData(const QString& passId) const { Q_ASSERT(d->zipFile); const auto passDir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("passes"))); if (!passDir) { return {}; } const auto file = passDir->file(passId + QLatin1String(".pkpass")); if (!file) { qCDebug(Log) << "pass not found" << passId; return {}; } return file->data(); } void File::addPass(KPkPass::Pass* pass, const QByteArray& rawData) { addPass(passId(pass), rawData); } void File::addPass(const QString &passId, const QByteArray& rawData) { Q_ASSERT(d->zipFile); d->zipFile->writeFile(QLatin1String("passes/") + passId + QLatin1String(".pkpass"), rawData); } QVector File::documents() const { const auto docDir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("documents"))); if (!docDir) { return {}; } const auto entries = docDir->entries(); QVector res; res.reserve(entries.size()); for (const auto &entry : entries) { if (docDir->entry(entry)->isDirectory()) { res.push_back(entry); } } return res; } QVariant File::documentInfo(const QString &id) const { Q_ASSERT(d->zipFile); const auto dir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("documents/") + id)); if (!dir) { return {}; } const auto file = dir->file(QStringLiteral("meta.json")); if (!file) { qCDebug(Log) << "document meta data not found" << id; return {}; } const auto doc = QJsonDocument::fromJson(file->data()); if (doc.isArray()) { const auto array = JsonLdDocument::fromJson(doc.array()); if (array.size() != 1) { qCWarning(Log) << "document meta data for" << id << "contains" << array.size() << "elements!"; return {}; } return array.at(0); } else if (doc.isObject()) { - return JsonLdDocument::fromJson(doc.object()); + return JsonLdDocument::fromJsonSingular(doc.object()); } return {}; } QByteArray File::documentData(const QString &id) const { const auto meta = documentInfo(id); if (!JsonLd::canConvert(meta)) { return {}; } const auto fileName = JsonLd::convert(meta).name(); const auto dir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("documents/") + id)); Q_ASSERT(dir); // checked by documentInfo already const auto file = dir->file(fileName); if (!file) { qCWarning(Log) << "document data not found" << id << fileName; return {}; } return file->data(); } QString File::normalizeDocumentFileName(const QString &name) { auto fileName = name; // normalize the filename to something we can safely deal with auto idx = fileName.lastIndexOf(QLatin1Char('/')); if (idx >= 0) { fileName = fileName.mid(idx + 1); } fileName.replace(QLatin1Char('?'), QLatin1Char('_')); fileName.replace(QLatin1Char('*'), QLatin1Char('_')); fileName.replace(QLatin1Char(' '), QLatin1Char('_')); fileName.replace(QLatin1Char('\\'), QLatin1Char('_')); if (fileName.isEmpty() || fileName == QLatin1String("meta.json")) { fileName = QStringLiteral("file"); } return fileName; } void File::addDocument(const QString &id, const QVariant &docInfo, const QByteArray &docData) { Q_ASSERT(d->zipFile); if (!JsonLd::canConvert(docInfo)) { qCWarning(Log) << "Invalid document meta data" << docInfo; return; } if (id.isEmpty()) { qCWarning(Log) << "Trying to add a document with an empty identifier!"; return; } const auto fileName = normalizeDocumentFileName(JsonLdDocument::readProperty(docInfo, "name").toString()); auto normalizedDocInfo = docInfo; JsonLdDocument::writeProperty(normalizedDocInfo, "name", fileName); d->zipFile->writeFile(QLatin1String("documents/") + id + QLatin1String("/meta.json"), QJsonDocument(JsonLdDocument::toJson(normalizedDocInfo)).toJson()); d->zipFile->writeFile(QLatin1String("documents/") + id + QLatin1Char('/') + fileName, docData); } QVector File::listCustomData(const QString &scope) const { Q_ASSERT(d->zipFile); const auto dir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("custom/") + scope)); if (!dir) { return {}; } const auto entries = dir->entries(); QVector res; res.reserve(entries.size()); std::copy(entries.begin(), entries.end(), std::back_inserter(res)); return res; } QByteArray File::customData(const QString& scope, const QString &id) const { Q_ASSERT(d->zipFile); const auto dir = dynamic_cast(d->zipFile->directory()->entry(QLatin1String("custom/") + scope)); if (!dir) { return {}; } const auto file = dir->file(id); if (!file) { qCDebug(Log) << "custom data not found" << scope << id; return {}; } return file->data(); } void File::addCustomData(const QString &scope, const QString &id, const QByteArray &data) { Q_ASSERT(d->zipFile); d->zipFile->writeFile(QLatin1String("custom/") + scope + QLatin1Char('/') + id, data); } diff --git a/src/generic/genericpkpassextractor.cpp b/src/generic/genericpkpassextractor.cpp index b79b8ee..a0f2207 100644 --- a/src/generic/genericpkpassextractor.cpp +++ b/src/generic/genericpkpassextractor.cpp @@ -1,291 +1,291 @@ /* Copyright (c) 2019 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "genericpkpassextractor_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static QVector frontFieldsForPass(KPkPass::Pass *pass) { QVector fields; fields += pass->headerFields(); fields += pass->primaryFields(); fields += pass->secondaryFields(); fields += pass->auxiliaryFields(); return fields; } static bool isAirportName(const QString &name, KnowledgeDb::IataCode iataCode) { if (name.size() <= 3) { return false; } const auto codes = KnowledgeDb::iataCodesFromName(name); return std::find(codes.begin(), codes.end(), iataCode) != codes.end(); } static bool isPlausibeGate(const QString &s) { for (const auto &c : s) { if (c.isLetter() || c.isDigit()) { return true; } } return false; } static Flight extractBoardingPass(KPkPass::Pass *pass, Flight flight) { // "relevantDate" is the best guess for the boarding time if (pass->relevantDate().isValid() && !flight.boardingTime().isValid()) { const auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{flight.departureAirport().iataCode()}); if (tz.isValid()) { flight.setBoardingTime(pass->relevantDate().toTimeZone(tz)); } else { flight.setBoardingTime(pass->relevantDate()); } } // search for missing information by field key const auto fields = pass->fields(); for (const auto &field : fields) { // boarding time if (!flight.boardingTime().isValid() && field.key().contains(QLatin1String("boarding"), Qt::CaseInsensitive)) { const auto time = QTime::fromString(field.value().toString()); if (time.isValid()) { // this misses date, but the postprocessor will fill that in flight.setBoardingTime(QDateTime(QDate(1, 1, 1), time)); continue; } } // departure gate if (flight.departureGate().isEmpty() && field.key().contains(QLatin1String("gate"), Qt::CaseInsensitive)) { const auto gateStr = field.value().toString(); if (isPlausibeGate(gateStr)) { flight.setDepartureGate(gateStr); continue; } } } // search for missing information in field content const auto depIata = KnowledgeDb::IataCode(flight.departureAirport().iataCode()); const auto arrIata = KnowledgeDb::IataCode(flight.arrivalAirport().iataCode()); const auto frontFields = frontFieldsForPass(pass); for (const auto &field : frontFields) { // full airport names if (flight.departureAirport().name().isEmpty()) { if (isAirportName(field.value().toString(), depIata)) { auto airport = flight.departureAirport(); airport.setName(field.value().toString()); flight.setDepartureAirport(airport); } else if (isAirportName(field.label(), depIata)) { auto airport = flight.departureAirport(); airport.setName(field.label()); flight.setDepartureAirport(airport); } } if (flight.arrivalAirport().name().isEmpty()) { if (isAirportName(field.value().toString(), arrIata)) { auto airport = flight.arrivalAirport(); airport.setName(field.value().toString()); flight.setArrivalAirport(airport); } else if (isAirportName(field.label(), arrIata)) { auto airport = flight.arrivalAirport(); airport.setName(field.label()); flight.setArrivalAirport(airport); } } } // location is the best guess for the departure airport geo coordinates auto depAirport = flight.departureAirport(); auto depGeo = depAirport.geo(); if (pass->locations().size() == 1 && !depGeo.isValid()) { const auto loc = pass->locations().at(0); depGeo.setLatitude(loc.latitude()); depGeo.setLongitude(loc.longitude()); depAirport.setGeo(depGeo); flight.setDepartureAirport(depAirport); } // organizationName is the best guess for airline name auto airline = flight.airline(); if (airline.name().isEmpty()) { airline.setName(pass->organizationName()); flight.setAirline(airline); } return flight; } static Event extractEventTicketPass(KPkPass::Pass *pass, Event event) { if (event.name().isEmpty()) { event.setName(pass->description()); } // "relevantDate" is the best guess for the start time if (pass->relevantDate().isValid() && !event.startDate().isValid()) { event.setStartDate(pass->relevantDate()); } // location is the best guess for the venue auto venue = event.location().value(); auto geo = venue.geo(); if (!pass->locations().isEmpty() && !geo.isValid()) { const auto loc = pass->locations().at(0); geo.setLatitude(loc.latitude()); geo.setLongitude(loc.longitude()); venue.setGeo(geo); if (venue.name().isEmpty()) { venue.setName(loc.relevantText()); } event.setLocation(venue); } return event; } static QDateTime iataContextDate(KPkPass::Pass *pass, const QDateTime &context) { if (!pass->relevantDate().isValid()) { return context; } return pass->relevantDate().addDays(-1); // go a bit back, to compensate for unknown departure timezone at this point } GenericExtractor::Result GenericPkPassExtractor::extract(KPkPass::Pass *pass, const QDateTime &contextDate) { QJsonObject result; if (auto boardingPass = qobject_cast(pass)) { switch (boardingPass->transitType()) { case KPkPass::BoardingPass::Air: result.insert(QStringLiteral("@type"), QLatin1String("FlightReservation")); break; case KPkPass::BoardingPass::Train: result.insert(QStringLiteral("@type"), QLatin1String("TrainReservation")); break; // TODO expand once we have test files for other types default: break; } } else { switch (pass->type()) { case KPkPass::Pass::EventTicket: result.insert(QStringLiteral("@type"), QLatin1String("EventReservation")); break; default: return {}; } } // barcode contains the ticket token if (!pass->barcodes().isEmpty()) { const auto barcode = pass->barcodes().at(0); QString token; switch (barcode.format()) { case KPkPass::Barcode::QR: token += QLatin1String("qrCode:"); break; case KPkPass::Barcode::Aztec: token += QLatin1String("aztecCode:"); break; default: break; } token += barcode.message(); QJsonObject ticket = result.value(QLatin1String("reservedTicket")).toObject(); ticket.insert(QStringLiteral("@type"), QLatin1String("Ticket")); ticket.insert(QStringLiteral("ticketToken"), token); result.insert(QStringLiteral("reservedTicket"), ticket); } // decode the barcode here already, so we have more information available for the following steps // also, we have additional context time information here - auto res = JsonLdDocument::fromJson(result); + auto res = JsonLdDocument::fromJsonSingular(result); if (JsonLd::isA(res)) { const auto bcbp = res.value().reservedTicket().value().ticketTokenData(); const auto bcbpData = IataBcbpParser::parse(bcbp, iataContextDate(pass, contextDate).date()); if (bcbpData.size() == 1) { res = JsonLdDocument::apply(bcbpData.at(0), res).value(); } else { // if this doesn't contain IATA BCBP data we wont be able to get sufficient information out of this return {}; } } // extract structured data from a pkpass, if the extractor script hasn't done so already switch (pass->type()) { case KPkPass::Pass::BoardingPass: { if (auto boardingPass = qobject_cast(pass)) { switch (boardingPass->transitType()) { case KPkPass::BoardingPass::Air: { auto flightRes = res.value(); flightRes.setReservationFor(extractBoardingPass(pass, flightRes.reservationFor().value())); res = flightRes; break; } default: break; } } break; } case KPkPass::Pass::EventTicket: { auto evRes = res.value(); evRes.setReservationFor(extractEventTicketPass(pass, evRes.reservationFor().value())); res = evRes; break; } default: break; } // associate the pass with the result, so we can find the pass again for display result = JsonLdDocument::toJson(res); if (!pass->passTypeIdentifier().isEmpty() && !pass->serialNumber().isEmpty()) { result.insert(QStringLiteral("pkpassPassTypeIdentifier"), pass->passTypeIdentifier()); result.insert(QStringLiteral("pkpassSerialNumber"), pass->serialNumber()); } return GenericExtractor::Result(QJsonArray({result})); } diff --git a/src/jsonlddocument.cpp b/src/jsonlddocument.cpp index 7496898..3ca13d1 100644 --- a/src/jsonlddocument.cpp +++ b/src/jsonlddocument.cpp @@ -1,486 +1,499 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "jsonlddocument.h" #include "jsonldimportfilter.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static QVariant createInstance(const QJsonObject &obj); // Eurowings workarounds... static const char* const fallbackDateTimePattern[] = { "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "MM-dd-yyyy HH:mm", // yes, seriously ;( "yyyyMMddTHHmmsst" }; static const auto fallbackDateTimePatternCount = sizeof(fallbackDateTimePattern) / sizeof(const char *); static double doubleValue(const QJsonValue &v) { if (v.isDouble()) { return v.toDouble(); } return v.toString().toDouble(); } static QVariant propertyValue(const QMetaProperty &prop, const QJsonValue &v) { switch (prop.type()) { case QVariant::String: return v.toString(); case QVariant::Date: return QDate::fromString(v.toString(), Qt::ISODate); case QVariant::DateTime: { QDateTime dt; if (v.isObject()) { const auto dtObj = v.toObject(); if (dtObj.value(QLatin1String("@type")).toString() == QLatin1String("QDateTime")) { dt = QDateTime::fromString(dtObj.value(QLatin1String("@value")).toString(), Qt::ISODate); dt.setTimeZone(QTimeZone(dtObj.value(QLatin1String("timezone")).toString().toUtf8())); } } else { auto str = v.toString(); dt = QDateTime::fromString(str, Qt::ISODate); for (unsigned int i = 0; i < fallbackDateTimePatternCount && dt.isNull(); ++i) { dt = QDateTime::fromString(str, QString::fromLatin1(fallbackDateTimePattern[i])); } // HACK QDateTimeParser handles 't' in the format but then forces it back to LocalTime in the end... if (dt.isValid() && dt.timeSpec() == Qt::LocalTime && str.endsWith(QLatin1Char('Z'))) { dt = dt.toTimeSpec(Qt::UTC); } if (dt.isNull()) { qCDebug(Log) << "Datetime parsing failed for" << str; } } return dt; } case QVariant::Double: return doubleValue(v); case QVariant::Int: if (v.isDouble()) { return v.toDouble(); } return v.toString().toInt(); case QVariant::Url: return QUrl(v.toString()); default: break; } if (prop.type() == qMetaTypeId()) { return doubleValue(v); } if (prop.userType() == qMetaTypeId()) { QVariantList l; if (v.isArray()) { const auto array = v.toArray(); l.reserve(array.size()); for (const auto &elem : array) { if (elem.isObject()) { const auto var = createInstance(elem.toObject()); if (!var.isNull()) { l.push_back(var); } } else if (elem.isString()) { l.push_back(elem.toString()); } } } return QVariant::fromValue(l); } return createInstance(v.toObject()); } static void createInstance(const QMetaObject *mo, void *v, const QJsonObject &obj) { for (auto it = obj.begin(); it != obj.end(); ++it) { if (it.key().startsWith(QLatin1Char('@'))) { continue; } const auto idx = mo->indexOfProperty(it.key().toLatin1().constData()); if (idx < 0) { qCDebug(Log) << "property" << it.key() << "could not be set on object of type" << mo->className(); continue; } const auto prop = mo->property(idx); const auto value = propertyValue(prop, it.value()); prop.writeOnGadget(v, value); } } template static QVariant createInstance(const QJsonObject &obj) { T t; createInstance(&T::staticMetaObject, &t, obj); return QVariant::fromValue(t); } #define MAKE_FACTORY(Class) \ if (type == QLatin1String(#Class)) \ return createInstance(obj) static QVariant createInstance(const QJsonObject &obj) { const auto type = obj.value(QLatin1String("@type")).toString(); MAKE_FACTORY(Action); MAKE_FACTORY(Airline); MAKE_FACTORY(Airport); MAKE_FACTORY(Brand); MAKE_FACTORY(BusReservation); MAKE_FACTORY(BusStation); MAKE_FACTORY(BusTrip); MAKE_FACTORY(CancelAction); MAKE_FACTORY(CheckInAction); MAKE_FACTORY(CreativeWork); MAKE_FACTORY(DigitalDocument); MAKE_FACTORY(DownloadAction); MAKE_FACTORY(EmailMessage); MAKE_FACTORY(Event); MAKE_FACTORY(EventReservation); MAKE_FACTORY(Flight); MAKE_FACTORY(FlightReservation); MAKE_FACTORY(FoodEstablishment); MAKE_FACTORY(FoodEstablishmentReservation); MAKE_FACTORY(LocalBusiness); MAKE_FACTORY(RentalCarReservation); MAKE_FACTORY(RentalCar); MAKE_FACTORY(ReserveAction); MAKE_FACTORY(GeoCoordinates); MAKE_FACTORY(LodgingBusiness); MAKE_FACTORY(LodgingReservation); MAKE_FACTORY(Organization); MAKE_FACTORY(Person); MAKE_FACTORY(Place); MAKE_FACTORY(PostalAddress); MAKE_FACTORY(Seat); MAKE_FACTORY(TaxiReservation); MAKE_FACTORY(Taxi); MAKE_FACTORY(Ticket); MAKE_FACTORY(TouristAttraction); MAKE_FACTORY(TouristAttractionVisit); MAKE_FACTORY(TrainReservation); MAKE_FACTORY(TrainStation); MAKE_FACTORY(TrainTrip); MAKE_FACTORY(UpdateAction); MAKE_FACTORY(ViewAction); if (type == QLatin1String("QDateTime")) { auto dt = QDateTime::fromString(obj.value(QLatin1String("@value")).toString(), Qt::ISODate); dt.setTimeZone(QTimeZone(obj.value(QLatin1String("timezone")).toString().toUtf8())); return dt; } return {}; } #undef MAKE_FACTORY - QVector JsonLdDocument::fromJson(const QJsonArray &array) { QVector l; l.reserve(array.size()); for (const auto &obj : array) { const auto v = fromJson(obj.toObject()); if (!v.isNull()) { l.push_back(v); } } return l; } QVariant JsonLdDocument::fromJson(const QJsonObject& obj) { - return createInstance(JsonLdImportFilter::filterObject(obj)); + const auto normalized = JsonLdImportFilter::filterObject(obj); + // TODO actually return a vector here + if (normalized.isEmpty()) { + return {}; + } + return createInstance(normalized.at(0).toObject()); +} + +QVariant JsonLdDocument::fromJsonSingular(const QJsonObject &obj) +{ + const auto normalized = JsonLdImportFilter::filterObject(obj); + if (normalized.isEmpty()) { + return {}; + } + return createInstance(normalized.at(0).toObject()); } static bool valueIsNull(const QVariant &v) { if (v.type() == QVariant::Url) { return !v.toUrl().isValid(); } if (v.type() == qMetaTypeId()) { return std::isnan(v.toFloat()); } return v.isNull(); } static QString typeName(const QMetaObject *mo, const QVariant &v) { const auto n = JsonLdDocument::readProperty(v, "className").toString(); if (!n.isEmpty()) { return n; } if (auto c = strstr(mo->className(), "::")) { return QString::fromUtf8(c + 2); } return QString::fromUtf8(mo->className()); } static QJsonValue toJsonValue(const QVariant &v) { const auto mo = QMetaType(v.userType()).metaObject(); if (!mo) { // basic types switch (v.type()) { case QVariant::String: return v.toString(); case QVariant::Double: return v.toDouble(); case QVariant::Int: return v.toInt(); case QVariant::Date: return v.toDate().toString(Qt::ISODate); case QVariant::DateTime: { const auto dt = v.toDateTime(); if (dt.timeSpec() == Qt::TimeZone) { QJsonObject dtObj; dtObj.insert(QStringLiteral("@type"), QStringLiteral("QDateTime")); dtObj.insert(QStringLiteral("@value"), dt.toString(Qt::ISODate)); dtObj.insert(QStringLiteral("timezone"), QString::fromUtf8(dt.timeZone().id())); return dtObj; } return v.toDateTime().toString(Qt::ISODate); } case QVariant::Url: return v.toUrl().toString(); case QVariant::Bool: return v.toBool(); default: break; } if (v.userType() == qMetaTypeId()) { return v.toFloat(); } if (v.canConvert()) { QSequentialIterable iterable = v.value(); if (iterable.size() == 0) { return {}; } QJsonArray array; for (const auto &var : iterable) { array.push_back(toJsonValue(var)); } return array; } qCDebug(Log) << "unhandled value:" << v; return {}; } // composite types QJsonObject obj; obj.insert(QStringLiteral("@type"), typeName(mo, v)); for (int i = 0; i < mo->propertyCount(); ++i) { const auto prop = mo->property(i); if (!prop.isStored()) { continue; } if (prop.isEnumType()) { // enums defined in this QMO const auto key = prop.readOnGadget(v.constData()).toInt(); const auto value = prop.enumerator().valueToKey(key); obj.insert(QString::fromUtf8(prop.name()), QString::fromUtf8(value)); continue; } else if (QMetaType::typeFlags(prop.userType()) & QMetaType::IsEnumeration) { // external enums obj.insert(QString::fromUtf8(prop.name()), prop.readOnGadget(v.constData()).toString()); continue; } const auto value = prop.readOnGadget(v.constData()); if (!valueIsNull(value)) { const auto jsVal = toJsonValue(value); if (jsVal.type() != QJsonValue::Null) { obj.insert(QString::fromUtf8(prop.name()), jsVal); } } } if (obj.size() > 1) { return obj; } return {}; } QJsonArray JsonLdDocument::toJson(const QVector &data) { QJsonArray a; for (const auto &d : data) { const auto value = ::toJsonValue(d); if (!value.isObject()) { continue; } auto obj = value.toObject(); obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org")); a.push_back(obj); } return a; } QJsonObject JsonLdDocument::toJson(const QVariant& data) { const auto value = ::toJsonValue(data); if (!value.isObject()) { return {}; } auto obj = value.toObject(); obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org")); return obj; } QVariant JsonLdDocument::readProperty(const QVariant &obj, const char *name) { const auto mo = QMetaType(obj.userType()).metaObject(); if (!mo) { return {}; } const auto idx = mo->indexOfProperty(name); if (idx < 0) { return {}; } const auto prop = mo->property(idx); return prop.readOnGadget(obj.constData()); } void JsonLdDocument::writeProperty(QVariant &obj, const char *name, const QVariant &value) { const auto mo = QMetaType(obj.userType()).metaObject(); if (!mo) { return; } writePropertyImpl(mo, obj.data(), name, value); } void JsonLdDocument::writePropertyImpl(const QMetaObject* mo, void* obj, const char* name, const QVariant& value) { const auto idx = mo->indexOfProperty(name); if (idx < 0) { return; } const auto prop = mo->property(idx); prop.writeOnGadget(obj, value); } void JsonLdDocument::removeProperty(QVariant &obj, const char *name) { writeProperty(obj, name, QVariant()); } QVariant JsonLdDocument::apply(const QVariant& lhs, const QVariant& rhs) { if (rhs.isNull()) { return lhs; } if (lhs.isNull()) { return rhs; } if (lhs.userType() != rhs.userType()) { qCWarning(Log) << "type mismatch during merging:" << lhs << rhs; return {}; } auto res = lhs; const auto mo = QMetaType(res.userType()).metaObject(); for (int i = 0; i < mo->propertyCount(); ++i) { const auto prop = mo->property(i); if (!prop.isStored()) { continue; } if (prop.isEnumType() && rhs.type() == QVariant::String) { // internal enums in this QMO const auto key = prop.enumerator().keyToValue(rhs.toString().toUtf8().constData()); prop.writeOnGadget(res.data(), key); continue; } if ((QMetaType::typeFlags(prop.userType()) & QMetaType::IsEnumeration) && rhs.type() == QVariant::String) { // external enums const QMetaType mt(prop.userType()); const auto mo = mt.metaObject(); if (!mo) { qCWarning(Log) << "No meta object found for enum type:" << prop.type(); continue; } const auto enumIdx = mo->indexOfEnumerator(prop.typeName() + strlen(mo->className()) + 2); if (enumIdx < 0) { qCWarning(Log) << "Could not find QMetaEnum for" << prop.type(); continue; } const auto me = mo->enumerator(enumIdx); bool success = false; const auto numValue = me.keyToValue(rhs.toString().toUtf8().constData(), &success); if (!success) { qCWarning(Log) << "Unknown enum value" << rhs.toString() << "for" << prop.type(); continue; } auto valueData = mt.create(); *reinterpret_cast(valueData) = numValue; QVariant value(prop.userType(), valueData); prop.writeOnGadget(res.data(), value); continue; } auto pv = prop.readOnGadget(rhs.constData()); if (QMetaType(pv.userType()).metaObject()) { pv = apply(prop.readOnGadget(lhs.constData()), pv); } if (!pv.isNull()) { prop.writeOnGadget(res.data(), pv); } } return res; } diff --git a/src/jsonlddocument.h b/src/jsonlddocument.h index 8c994e7..fc5ac94 100644 --- a/src/jsonlddocument.h +++ b/src/jsonlddocument.h @@ -1,79 +1,88 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KITINERARY_JSONLDDOCUMENT_H #define KITINERARY_JSONLDDOCUMENT_H #include "kitinerary_export.h" #include #include class QJsonArray; class QJsonObject; struct QMetaObject; namespace KItinerary { /** Serialization/deserialization code for JSON-LD data. * @see https://www.w3.org/TR/json-ld/ */ class JsonLdDocument { public: /** Convert JSON-LD array into instantiated data types. */ static KITINERARY_EXPORT QVector fromJson(const QJsonArray &array); /** Convert JSON-LD object into an instantiated data type. */ - static KITINERARY_EXPORT QVariant fromJson(const QJsonObject &obj); + static KITINERARY_EXPORT QVariant fromJson(const QJsonObject &obj); // TODO return QVector here + + /** Convert a single JSON-LD object into an instantiated data type. + * @note Use this only if you are sure the JSON-LD object does not expand to multiple objects! + * That is usually only the case for objects you have written yourself and that semantically + * are guaranteed to be a single object. Anything received from external sources can expand + * and should not use this method. + * @since 20.04 + */ + static KITINERARY_EXPORT QVariant fromJsonSingular(const QJsonObject &obj); /** Serialize instantiated data types to JSON-LD. */ static KITINERARY_EXPORT QJsonArray toJson(const QVector &data); /** Serialize instantiated data type to JSON-LD. */ static KITINERARY_EXPORT QJsonObject toJson(const QVariant &data); /** Read property @p name on object @p obj. */ static KITINERARY_EXPORT QVariant readProperty(const QVariant &obj, const char *name); /** Set property @p name on object @p obj to value @p value. */ static KITINERARY_EXPORT void writeProperty(QVariant &obj, const char *name, const QVariant &value); /** Set property @p name on object @p obj to value @p value. */ template inline static void writeProperty(T &obj, const char *name, const QVariant &value); /** Removes property @p name on object @p obj. */ KITINERARY_EXPORT static void removeProperty(QVariant &obj, const char *name); /** Apply all properties of @p rhs on to @p lhs. * Use this to merge two top-level objects of the same type, with * @p rhs containing newer information. */ KITINERARY_EXPORT static QVariant apply(const QVariant &lhs, const QVariant &rhs); private: KITINERARY_EXPORT static void writePropertyImpl(const QMetaObject *mo, void *obj, const char *name, const QVariant &value); }; template inline void JsonLdDocument::writeProperty(T &obj, const char *name, const QVariant &value) { writePropertyImpl(&T::staticMetaObject, &obj, name, value); } } #endif // KITINERARY_JSONLDDOCUMENT_H diff --git a/src/jsonldimportfilter.cpp b/src/jsonldimportfilter.cpp index 7b72eb8..6d1af4a 100644 --- a/src/jsonldimportfilter.cpp +++ b/src/jsonldimportfilter.cpp @@ -1,359 +1,379 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "jsonldimportfilter.h" #include "logging.h" #include #include #include #include #include #include using namespace KItinerary; // type normalization from full schema.org type hierarchy to our simplified subset // IMPORTANT: keep alphabetically sorted by fromType! static const struct { const char* fromType; const char* toType; } type_mapping[] = { { "Bakery", "FoodEstablishment" }, { "BarOrPub", "FoodEstablishment" }, { "BedAndBreakfast", "LodgingBusiness" }, { "Brewery", "FoodEstablishment" }, { "BusStop", "BusStation" }, { "BusinessEvent", "Event" }, { "CafeOrCoffeeShop", "FoodEstablishment" }, { "Campground", "LodgingBusiness" }, { "ChildrensEvent", "Event" }, { "ComedyEvent", "Event" }, { "DanceEvent", "Event" }, { "Distillery", "FoodEstablishment" }, { "EditAction", "UpdateAction" }, { "EducationEvent", "Event" }, { "ExhibitionEvent", "Event" }, { "FastFoodRestaurant", "FoodEstablishment" }, { "Festival", "Event" }, { "Hostel", "LodgingBusiness" }, { "Hotel", "LodgingBusiness" }, { "IceCreamShop", "FoodEstablishment" }, { "LiteraryEvent", "Event" }, { "Motel", "LodgingBusiness" }, { "MusicEvent", "Event" }, { "Resort", "LodgingBusiness" }, { "Restaurant", "FoodEstablishment" }, { "SaleEvent", "Event" }, { "ScreeningEvent", "Event" }, { "SocialEvent", "Event" }, { "SportsEvent", "Event" }, { "TheaterEvent", "Event" }, { "VisualArtsEvent", "Event" }, { "Winery", "FoodEstablishment" }, }; static void renameProperty(QJsonObject &obj, const char *oldName, const char *newName) { const auto value = obj.value(QLatin1String(oldName)); if (!value.isNull() && !obj.contains(QLatin1String(newName))) { obj.insert(QLatin1String(newName), value); obj.remove(QLatin1String(oldName)); } } static void migrateToAction(QJsonObject &obj, const char *propName, const char *typeName, bool remove) { const auto value = obj.value(QLatin1String(propName)); if (value.isNull() || value.isUndefined()) { return; } auto actions = obj.value(QLatin1String("potentialAction")).toArray(); for (const auto &act : actions) { if (act.toObject().value(QLatin1String("@type")).toString() == QLatin1String(typeName)) { return; } } QJsonObject action; action.insert(QStringLiteral("@type"), QLatin1String(typeName)); action.insert(QStringLiteral("target"), value); actions.push_back(action); obj.insert(QStringLiteral("potentialAction"), actions); if (remove) { obj.remove(QStringLiteral("propName")); } } static void filterTrainTrip(QJsonObject &trip) { // move TrainTrip::trainCompany to TrainTrip::provider (as defined by schema.org) renameProperty(trip, "trainCompany", "provider"); } static void filterLodgingReservation(QJsonObject &res) { // check[in|out]Date -> check[in|out]Time (legacy Google format) renameProperty(res, "checkinDate", "checkinTime"); renameProperty(res, "checkoutDate", "checkoutTime"); } static void filterFlight(QJsonObject &res) { // move incomplete departureTime (ie. just ISO date, no time) to departureDay if (res.value(QLatin1String("departureTime")).toString().size() == 10) { renameProperty(res, "departureTime", "departureDay"); } } static void filterReservation(QJsonObject &res) { // move ticketToken to Ticket (Google vs. schema.org difference) const auto token = res.value(QLatin1String("ticketToken")).toString(); if (!token.isEmpty()) { auto ticket = res.value(QLatin1String("reservedTicket")).toObject(); if (ticket.isEmpty()) { ticket.insert(QStringLiteral("@type"), QLatin1String("Ticket")); } if (!ticket.contains(QLatin1String("ticketToken"))) { ticket.insert(QStringLiteral("ticketToken"), token); res.insert(QStringLiteral("reservedTicket"), ticket); res.remove(QStringLiteral("ticketToken")); } } // unpack reservationFor array - if we ever encounter more than one element in here we'd need to multiply the result const auto resFor = res.value(QLatin1String("reservationFor")); if (resFor.isArray()) { const auto a = resFor.toArray(); if (a.size() > 1) { qCWarning(Log) << "Found reservationFor array with" << a.size() << "elements!"; } if (!a.isEmpty()) { res.insert(QStringLiteral("reservationFor"), a.at(0)); } } // legacy potentialAction property renameProperty(res, "action", "potentialAction"); // move Google xxxUrl properties to Action instances migrateToAction(res, "cancelReservationUrl", "CancelAction", true); migrateToAction(res, "checkinUrl", "CheckInAction", true); migrateToAction(res, "modifyReservationUrl", "UpdateAction", true); migrateToAction(res, "ticketDownloadUrl", "DownloadAction", true); migrateToAction(res, "url", "ViewAction", false); // technically the wrong way (reservationId is the current schema.org standard), but hardly used anywhere (yet) renameProperty(res, "reservationId", "reservationNumber"); // "typos" renameProperty(res, "Url", "url"); } static void filterBusTrip(QJsonObject &trip) { renameProperty(trip, "arrivalStation", "arrivalBusStop"); renameProperty(trip, "departureStation", "departureBusStop"); renameProperty(trip, "busCompany", "provider"); } static void filterFoodEstablishment(QJsonObject &restaurant) { // This can be a bool, "Yes"/"No", or a URL. auto reservationsValue = restaurant.value(QLatin1String("acceptsReservations")); if (reservationsValue.isString()) { const QString reservations = reservationsValue.toString(); if (reservations == QLatin1String("Yes")) { restaurant.insert(QLatin1String("acceptsReservations"), true); } else if (reservations == QLatin1String("No")) { restaurant.insert(QLatin1String("acceptsReservations"), false); } else { migrateToAction(restaurant, "acceptsReservations", "ReserveAction", true); } } } static void filterActionTarget(QJsonObject &action) { QJsonArray targets; QString filteredTargetUrlString; const QJsonValue oldTarget = action.value(QLatin1String("target")); if (oldTarget.isArray()) { targets = oldTarget.toArray(); } else if (oldTarget.isObject()) { targets.push_back(oldTarget); } for (auto it = targets.begin(); it != targets.end(); ++it) { auto target = (*it).toObject(); QJsonArray platforms; const QJsonValue actionPlatform = target.value(QLatin1String("actionPlatform")); if (actionPlatform.isArray()) { platforms = actionPlatform.toArray(); } else { platforms.push_back(actionPlatform); } // Always return at least one URL but prefer the current platform if possible if (!filteredTargetUrlString.isEmpty()) { const bool hasPreferredPlatform = std::any_of(platforms.begin(), platforms.end(), [](const QJsonValue &platformValue) { const QString platform = platformValue.toString(); // FIXME android return platform == QLatin1String("http://schema.org/DesktopWebPlatform"); }); if (!hasPreferredPlatform) { continue; } } const QUrl url(target.value(QLatin1String("urlTemplate")).toString()); // It could also be a "URL template" if (!url.isValid()) { continue; } filteredTargetUrlString = url.toString(); } if (filteredTargetUrlString.isEmpty()) { renameProperty(action, "url", "target"); } else { action.insert(QStringLiteral("target"), filteredTargetUrlString); } } static QJsonArray filterActions(const QJsonValue &v) { QJsonArray actions; if (v.isArray()) { actions = v.toArray(); } else { actions.push_back(v); } for (auto it = actions.begin(); it != actions.end(); ++it) { auto action = (*it).toObject(); filterActionTarget(action); *it = action; } return actions; } // filter functions applied to objects of the corresponding (already normalized) type // IMPORTANT: keep alphabetically sorted by type! static const struct { const char* type; void(*filterFunc)(QJsonObject&); } type_filters[] = { { "BusTrip", filterBusTrip }, { "Flight", filterFlight }, { "FoodEstablishment", filterFoodEstablishment }, { "LodgingReservation", filterLodgingReservation }, { "TrainTrip", filterTrainTrip }, }; static void filterRecursive(QJsonObject &obj); static void filterRecursive(QJsonArray &array) { for (auto it = array.begin(); it != array.end(); ++it) { if ((*it).type() == QJsonValue::Object) { QJsonObject subObj = (*it).toObject(); filterRecursive(subObj); *it = subObj; } else if ((*it).type() == QJsonValue::Array) { QJsonArray array = (*it).toArray(); filterRecursive(array); *it = array; } } } static void filterRecursive(QJsonObject &obj) { auto type = obj.value(QLatin1String("@type")).toString().toUtf8(); // normalize type const auto it = std::lower_bound(std::begin(type_mapping), std::end(type_mapping), type, [](const auto &lhs, const auto &rhs) { return std::strcmp(lhs.fromType, rhs.constData()) < 0; }); if (it != std::end(type_mapping) && std::strcmp((*it).fromType, type.constData()) == 0) { type = it->toType; obj.insert(QStringLiteral("@type"), QLatin1String(type)); } for (auto it = obj.begin(); it != obj.end(); ++it) { if ((*it).type() == QJsonValue::Object) { QJsonObject subObj = (*it).toObject(); filterRecursive(subObj); *it = subObj; } else if ((*it).type() == QJsonValue::Array) { QJsonArray array = (*it).toArray(); filterRecursive(array); *it = array; } } // apply filter functions const auto filterIt = std::lower_bound(std::begin(type_filters), std::end(type_filters), type, [](const auto &lhs, const auto &rhs) { return std::strcmp(lhs.type, rhs.constData()) < 0; }); if (filterIt != std::end(type_filters) && std::strcmp((*filterIt).type, type.constData()) == 0) { (*filterIt).filterFunc(obj); } } -QJsonObject JsonLdImportFilter::filterObject(const QJsonObject& obj) +QJsonArray JsonLdImportFilter::filterObject(const QJsonObject& obj) { - QJsonObject res(obj); - filterRecursive(res); - - const auto type = obj.value(QLatin1String("@type")).toString(); - if (type.endsWith(QLatin1String("Reservation"))) { - filterReservation(res); + QStringList types; + const auto typeVal = obj.value(QLatin1String("@type")); + if (typeVal.isString()) { + types.push_back(typeVal.toString()); + } else if (typeVal.isArray()) { + const auto typeNames = typeVal.toArray(); + for (const auto &t : typeNames) { + if (t.isString()) { + types.push_back(t.toString()); + } + } } + // TODO consider additionalTypes property - auto actions = res.value(QLatin1String("potentialAction")); - if (!actions.isUndefined()) { - res.insert(QStringLiteral("potentialAction"), filterActions(actions)); - } + QJsonArray results; - auto image = res.value(QLatin1String("image")); - if (image.isArray()) { - res.insert(QStringLiteral("image"), image.toArray().first()); - } + for (const auto &type : types) { + QJsonObject res(obj); + res.insert(QStringLiteral("@type"), type); + filterRecursive(res); + + if (type.endsWith(QLatin1String("Reservation"))) { + filterReservation(res); + } - image = res.value(QLatin1String("image")); - if (image.isObject()) { - const auto imageObject = image.toObject(); - if (imageObject.value(QLatin1String("@type")).toString() == QLatin1String("ImageObject")) { - res.insert(QStringLiteral("image"), imageObject.value(QLatin1String("url"))); + auto actions = res.value(QLatin1String("potentialAction")); + if (!actions.isUndefined()) { + res.insert(QStringLiteral("potentialAction"), filterActions(actions)); } + + auto image = res.value(QLatin1String("image")); + if (image.isArray()) { + res.insert(QStringLiteral("image"), image.toArray().first()); + } + + image = res.value(QLatin1String("image")); + if (image.isObject()) { + const auto imageObject = image.toObject(); + if (imageObject.value(QLatin1String("@type")).toString() == QLatin1String("ImageObject")) { + res.insert(QStringLiteral("image"), imageObject.value(QLatin1String("url"))); + } + } + + results.push_back(res); } - return res; + return results; } diff --git a/src/jsonldimportfilter.h b/src/jsonldimportfilter.h index 8e4a92f..cbeabbc 100644 --- a/src/jsonldimportfilter.h +++ b/src/jsonldimportfilter.h @@ -1,36 +1,39 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KITINERARY_JSONLDIMPORTFILTER_H #define KITINERARY_JSONLDIMPORTFILTER_H +class QJsonArray; class QJsonObject; namespace KItinerary { /** Filter input JSON for loading with JsonLdDocument, to deal with obsolete * or renamed elements. */ namespace JsonLdImportFilter { - /** Filter the top-level object @p obj for loading with JsonLdDocument. */ - QJsonObject filterObject(const QJsonObject &obj); + /** Filter the top-level object @p obj for loading with JsonLdDocument. + * Due to type and graph expansion, the result can actually contain multiple object. + */ + QJsonArray filterObject(const QJsonObject &obj); } } #endif // KITINERARY_JSONLDIMPORTFILTER_H