diff --git a/autotests/pkpassdata/airbaltic.json b/autotests/pkpassdata/airbaltic.json index 18edf62..645cf7e 100644 --- a/autotests/pkpassdata/airbaltic.json +++ b/autotests/pkpassdata/airbaltic.json @@ -1,49 +1,48 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "14E", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "BT" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 56.92359924316406, "longitude": 23.971099853515625 }, "iataCode": "RIX" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-11-05T07:25:00+01:00", "timezone": "Europe/Berlin" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, "iataCode": "TXL" }, "departureDay": "2017-11-05", "flightNumber": "212" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLRIXBT 0212 309Y014E0063 100" }, - "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLRIXBT 0212 309Y014E0063 100", "underName": { "@type": "Person", "name": "KRAUSE/VOLKER" } } ] diff --git a/autotests/pkpassdata/airberlin.json b/autotests/pkpassdata/airberlin.json index fcd03ca..0ea70b3 100644 --- a/autotests/pkpassdata/airberlin.json +++ b/autotests/pkpassdata/airberlin.json @@ -1,49 +1,48 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "19F", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "AB" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 48.35390090942383, "longitude": 11.786100387573242 }, "iataCode": "MUC" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-10-24T15:55:00+02:00", "timezone": "Europe/Berlin" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 52.559688568115234, "longitude": 13.287711143493652 }, "iataCode": "TXL" }, "departureDay": "2017-10-24", "flightNumber": "6203" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLMUCAB 6203 297Y019F0060 33C>5080 B2A N00 " }, - "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLMUCAB 6203 297Y019F0060 33C>5080 B2A N00 ", "underName": { "@type": "Person", "name": "KRAUSE/VOLKER" } } ] diff --git a/autotests/pkpassdata/eurowings.json b/autotests/pkpassdata/eurowings.json index 2492921..4d2a8a5 100644 --- a/autotests/pkpassdata/eurowings.json +++ b/autotests/pkpassdata/eurowings.json @@ -1,53 +1,52 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "17C", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "4U", "name": "Germanwings" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, "iataCode": "TXL", "name": "Berlin-Tegel" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-06-18T18:40:00+01:00", "timezone": "Europe/London" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 51.477500915527344, "longitude": -0.4613890051841736 }, "iataCode": "LHR", "name": "London Heathrow" }, "departureDay": "2017-06-18", "departureGate": " - ", "flightNumber": "8465" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 LHRTXL4U 8465 169Y017C0040 147>1181 7168B4U 0000000000000291040PASSPORTID2 LH 123412341234012 " }, - "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 LHRTXL4U 8465 169Y017C0040 147>1181 7168B4U 0000000000000291040PASSPORTID2 LH 123412341234012 ", "underName": { "@type": "Person", "name": "KRAUSE/VOLKER" } } ] diff --git a/autotests/pkpassdata/lufthansa.json b/autotests/pkpassdata/lufthansa.json index 37ce7c9..6670dcf 100644 --- a/autotests/pkpassdata/lufthansa.json +++ b/autotests/pkpassdata/lufthansa.json @@ -1,52 +1,51 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "5A", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "LH" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, "iataCode": "TXL", "name": "BERLIN" }, "boardingTime": { "@type": "QDateTime", "@value": "2018-03-06T17:05:00+01:00", "timezone": "Europe/Berlin" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 48.35390090942383, "longitude": 11.786100387573242 }, "iataCode": "MUC", "name": "MUNICH" }, "departureDay": "2018-03-06", "departureGate": "G20", "flightNumber": "2724" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1DOE/JOHN EXXX007 MUCTXLLH 2724 065M005A0017 35D>6180WW8064BLH 2A22012345678900 LH N*30600000K09 " }, - "ticketToken": "aztecCode:M1DOE/JOHN EXXX007 MUCTXLLH 2724 065M005A0017 35D>6180WW8064BLH 2A22012345678900 LH N*30600000K09 ", "underName": { "@type": "Person", "name": "DOE/JOHN" } } ] diff --git a/autotests/pkpassdata/swiss.json b/autotests/pkpassdata/swiss.json index a68fdca..5e0667b 100644 --- a/autotests/pkpassdata/swiss.json +++ b/autotests/pkpassdata/swiss.json @@ -1,54 +1,53 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "10E", "pkpassPassTypeIdentifier": "pass.booking.swiss.com", "pkpassSerialNumber": "123456789", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "LX" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, "iataCode": "TXL", "name": "destinationHeading" }, "boardingTime": { "@type": "QDateTime", "@value": "2018-09-15T20:25:00+02:00", "timezone": "Europe/Zurich" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 47.452301025390625, "longitude": 8.560830116271973 }, "iataCode": "ZRH", "name": "departsHeading" }, "departureDay": "2018-09-15", "departureGate": "AB", "flightNumber": "962" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 ZRHTXLLX 0962 258Y010E0143 15D>5180 M7258BLX 2A724xxxxxxxxxx0 LX LH 123456789012345 N*30600000K09 " }, - "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 ZRHTXLLX 0962 258Y010E0143 15D>5180 M7258BLX 2A724xxxxxxxxxx0 LX LH 123456789012345 N*30600000K09 ", "underName": { "@type": "Person", "name": "KRAUSE/VOLKER" } } ] diff --git a/autotests/postprocessordata/bcbp-expansion.post.json b/autotests/postprocessordata/bcbp-expansion.post.json index 0ad3676..e6d7d5b 100644 --- a/autotests/postprocessordata/bcbp-expansion.post.json +++ b/autotests/postprocessordata/bcbp-expansion.post.json @@ -1,49 +1,48 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "1A", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "AC" }, "arrivalAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 50.03329849243164, "longitude": 8.570560455322266 }, "iataCode": "FRA" }, "boardingTime": { "@type": "QDateTime", "@value": "2027-03-05T06:30:00-05:00", "timezone": "America/Toronto" }, "departureAirport": { "@type": "Airport", "geo": { "@type": "GeoCoordinates", "latitude": 45.47060012817383, "longitude": -73.74079895019531 }, "iataCode": "YUL" }, "departureDay": "2027-03-05", "flightNumber": "834" }, "reservationNumber": "ABC123", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1DESMARAIS/LUC EABC123 YULFRAAC 0834 326J001A0025 100" }, "underName": { "@type": "Person", "name": "DESMARAIS/LUC" - }, - "ticketToken": "aztecCode:M1DESMARAIS/LUC EABC123 YULFRAAC 0834 326J001A0025 100" + } } ] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 934a0e2..796f52e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,111 +1,112 @@ set(KDE_INSTALL_INCLUDEDIR_PIM ${KDE_INSTALL_INCLUDEDIR}/KPim) add_subdirectory(airportdb) if(TARGET Poppler::Qt5) set(HAVE_POPPLER ON) endif() configure_file(config-kitinerary.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kitinerary.h) set(kitinerary_lib_srcs airportdb/airportdb.cpp datatypes/bustrip.cpp datatypes/flight.cpp datatypes/organization.cpp datatypes/person.cpp datatypes/place.cpp datatypes/reservation.cpp datatypes/ticket.cpp datatypes/traintrip.cpp calendarhandler.cpp extractor.cpp extractorengine.cpp extractorfilter.cpp extractorpreprocessor.cpp extractorpostprocessor.cpp extractorrepository.cpp iatabcbpparser.cpp jsonlddocument.cpp + jsonldimportfilter.cpp mergeutil.cpp structureddataextractor.cpp ) qt5_add_resources(kitinerary_lib_srcs extractors/extractors.qrc) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER logging.h IDENTIFIER KItinerary::Log CATEGORY_NAME org.kde.kitinerary) add_library(KPimItinerary SHARED ${kitinerary_lib_srcs}) add_library(KPim::Itinerary ALIAS KPimItinerary) generate_export_header(KPimItinerary BASE_NAME KItinerary) set_target_properties(KPimItinerary PROPERTIES VERSION ${KITINERARY_VERSION_STRING} SOVERSION ${KITINERARY_SOVERSION} EXPORT_NAME Itinerary ) target_include_directories(KPimItinerary INTERFACE "$") target_include_directories(KPimItinerary PUBLIC "$") target_link_libraries(KPimItinerary PUBLIC Qt5::Core KF5::Mime KF5::CalendarCore PRIVATE Qt5::Qml KF5::I18n KPim::PkPass ) if (HAVE_POPPLER) target_link_libraries(KPimItinerary PRIVATE Poppler::Qt5) endif() ecm_generate_headers(KItinerary_FORWARDING_HEADERS HEADER_NAMES CalendarHandler Extractor ExtractorEngine ExtractorPreprocessor ExtractorPostprocessor ExtractorRepository JsonLdDocument MergeUtil StructuredDataExtractor PREFIX KItinerary REQUIRED_HEADERS KItinerary_HEADERS ) ecm_generate_headers(KItinerary_AirportDb_FORWARDING_HEADERS HEADER_NAMES AirportDb PREFIX KItinerary REQUIRED_HEADERS KItinerary_AirportDb_HEADERS RELATIVE airportdb ) ecm_generate_headers(KItinerary_Datatypes_FORWARDING_HEADERS HEADER_NAMES BusTrip Datatypes Flight Organization Reservation Person Place Ticket TrainTrip PREFIX KItinerary REQUIRED_HEADERS KItinerary_Datatypes_HEADERS RELATIVE datatypes ) install(TARGETS KPimItinerary EXPORT KPimItineraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KItinerary_FORWARDING_HEADERS} ${KItinerary_AirportDb_FORWARDING_HEADERS} ${KItinerary_Datatypes_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary ) install(FILES ${KItinerary_HEADERS} ${KItinerary_AirportDb_HEADERS} ${KItinerary_Datatypes_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kitinerary_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/kitinerary ) diff --git a/src/datatypes/reservation.cpp b/src/datatypes/reservation.cpp index c263156..71d1f5f 100644 --- a/src/datatypes/reservation.cpp +++ b/src/datatypes/reservation.cpp @@ -1,117 +1,115 @@ /* 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 "reservation.h" #include "organization.h" #include "datatypes_p.h" #include #include using namespace KItinerary; namespace KItinerary { class ReservationPrivate : public QSharedData { KITINERARY_PRIVATE_BASE_GADGET(Reservation) public: QString reservationNumber; QVariant reservationFor; QVariant reservedTicket; QVariant underName; + QUrl url; QUrl cancelReservationUrl; QUrl modifyReservationUrl; - QString ticketToken; - QUrl url; QString pkpassPassTypeIdentifier; QString pkpassSerialNumber; Organization provider; }; KITINERARY_MAKE_BASE_CLASS(Reservation) KITINERARY_MAKE_PROPERTY(Reservation, QString, reservationNumber, setReservationNumber) KITINERARY_MAKE_PROPERTY(Reservation, QVariant, reservationFor, setReservationFor) KITINERARY_MAKE_PROPERTY(Reservation, QVariant, reservedTicket, setReservedTicket) KITINERARY_MAKE_PROPERTY(Reservation, QVariant, underName, setUnderName) KITINERARY_MAKE_PROPERTY(Reservation, QUrl, cancelReservationUrl, setCancelReservationUrl) KITINERARY_MAKE_PROPERTY(Reservation, QUrl, modifyReservationUrl, setModifyReservationUrl) -KITINERARY_MAKE_PROPERTY(Reservation, QString, ticketToken, setTicketToken) KITINERARY_MAKE_PROPERTY(Reservation, QUrl, url, setUrl) KITINERARY_MAKE_PROPERTY(Reservation, QString, pkpassPassTypeIdentifier, setPkpassPassTypeIdentifier) KITINERARY_MAKE_PROPERTY(Reservation, QString, pkpassSerialNumber, setPkpassSerialNumber) KITINERARY_MAKE_PROPERTY(Reservation, Organization, provider, setProvider) class LodgingReservationPrivate : public ReservationPrivate { KITINERARY_PRIVATE_GADGET(LodgingReservation) public: QDateTime checkinDate; QDateTime checkoutDate; }; KITINERARY_MAKE_SUB_CLASS(LodgingReservation, Reservation) KITINERARY_MAKE_PROPERTY(LodgingReservation, QDateTime, checkinDate, setCheckinDate) KITINERARY_MAKE_PROPERTY(LodgingReservation, QDateTime, checkoutDate, setCheckoutDate) QString LodgingReservation::checkinDateLocalized() const { K_D(const LodgingReservation); return QLocale().toString(d->checkinDate.date(), QLocale::ShortFormat); } QString LodgingReservation::checkoutDateLocalized() const { K_D(const LodgingReservation); return QLocale().toString(d->checkoutDate.date(), QLocale::ShortFormat); } class FlightReservationPrivate : public ReservationPrivate { KITINERARY_PRIVATE_GADGET(FlightReservation) public: QString airplaneSeat; QString boardingGroup; QUrl ticketDownloadUrl; }; KITINERARY_MAKE_SUB_CLASS(FlightReservation, Reservation) KITINERARY_MAKE_PROPERTY(FlightReservation, QString, airplaneSeat, setAirplaneSeat) KITINERARY_MAKE_PROPERTY(FlightReservation, QString, boardingGroup, setBoardingGroup) KITINERARY_MAKE_PROPERTY(FlightReservation, QUrl, ticketDownloadUrl, setTicketDownloadUrl) class TrainReservationPrivate : public ReservationPrivate { KITINERARY_PRIVATE_GADGET(TrainReservation) }; KITINERARY_MAKE_SUB_CLASS(TrainReservation, Reservation) class BusReservationPrivate : public ReservationPrivate { KITINERARY_PRIVATE_GADGET(BusReservation) }; KITINERARY_MAKE_SUB_CLASS(BusReservation, Reservation) } template <> KItinerary::ReservationPrivate *QExplicitlySharedDataPointer::clone() { return d->clone(); } #include "moc_reservation.cpp" diff --git a/src/datatypes/reservation.h b/src/datatypes/reservation.h index 4bb7a4d..82e0ea8 100644 --- a/src/datatypes/reservation.h +++ b/src/datatypes/reservation.h @@ -1,122 +1,119 @@ /* 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_RESERVATION_H #define KITINERARY_RESERVATION_H #include "kitinerary_export.h" #include "datatypes.h" class QUrl; namespace KItinerary { class ReservationPrivate; class Organization; /** Abstract base class for reservations. * @see https://schema.org/Reservation */ class KITINERARY_EXPORT Reservation { KITINERARY_BASE_GADGET(Reservation) KITINERARY_PROPERTY(QString, reservationNumber, setReservationNumber) KITINERARY_PROPERTY(QVariant, reservationFor, setReservationFor) KITINERARY_PROPERTY(QVariant, reservedTicket, setReservedTicket) KITINERARY_PROPERTY(QVariant, underName, setUnderName) KITINERARY_PROPERTY(Organization, provider, setProvider) + KITINERARY_PROPERTY(QUrl, url, setUrl) // Google extension KITINERARY_PROPERTY(QUrl, cancelReservationUrl, setCancelReservationUrl) KITINERARY_PROPERTY(QUrl, modifyReservationUrl, setModifyReservationUrl) - KITINERARY_PROPERTY(QString, ticketToken, setTicketToken) - KITINERARY_PROPERTY(QUrl, url, setUrl) // KDE extensions - /** @property pkpassPassTypeIdentifier pass type identifier of an associated - * Apple Wallet boarding pass. + /** Pass type identifier of an associated Apple Wallet boarding pass. * @see KPkPass::Pass::passTypeIdentifier */ KITINERARY_PROPERTY(QString, pkpassPassTypeIdentifier, setPkpassPassTypeIdentifier) - /** @property pkpassSerialNumber serial number of an associated Apple Wallet - * boarding pass. + /** Serial number of an associated Apple Wallet boarding pass. * @see KPkPass::Pass::serialNumber */ KITINERARY_PROPERTY(QString, pkpassSerialNumber, setPkpassSerialNumber) protected: ///@cond internal QExplicitlySharedDataPointer d; ///@endcond }; /** A hotel reservation. * @see https://schema.org/LodgingReservation */ class KITINERARY_EXPORT LodgingReservation : public Reservation { KITINERARY_GADGET(LodgingReservation) KITINERARY_PROPERTY(QDateTime, checkinDate, setCheckinDate) KITINERARY_PROPERTY(QDateTime, checkoutDate, setCheckoutDate) Q_PROPERTY(QString checkinDateLocalized READ checkinDateLocalized STORED false CONSTANT) Q_PROPERTY(QString checkoutDateLocalized READ checkoutDateLocalized STORED false CONSTANT) private: QString checkinDateLocalized() const; QString checkoutDateLocalized() const; }; /** A flight reservation. * @see https://schema.org/FlightReservation * @see https://developers.google.com/gmail/markup/reference/flight-reservation */ class KITINERARY_EXPORT FlightReservation : public Reservation { KITINERARY_GADGET(FlightReservation) // Google extensions KITINERARY_PROPERTY(QString, airplaneSeat, setAirplaneSeat) KITINERARY_PROPERTY(QString, boardingGroup, setBoardingGroup) KITINERARY_PROPERTY(QUrl, ticketDownloadUrl, setTicketDownloadUrl) }; /** A train reservation. * @see https://schema.org/TrainReservation */ class KITINERARY_EXPORT TrainReservation : public Reservation { KITINERARY_GADGET(TrainReservation) }; /** A bus reservation. * @see https://schema.org/BusReservation */ class KITINERARY_EXPORT BusReservation : public Reservation { KITINERARY_GADGET(BusReservation) }; } Q_DECLARE_METATYPE(KItinerary::FlightReservation) Q_DECLARE_METATYPE(KItinerary::LodgingReservation) Q_DECLARE_METATYPE(KItinerary::TrainReservation) Q_DECLARE_METATYPE(KItinerary::BusReservation) #endif // KITINERARY_RESERVATION_H diff --git a/src/extractorengine.cpp b/src/extractorengine.cpp index 2e72641..8d8d260 100644 --- a/src/extractorengine.cpp +++ b/src/extractorengine.cpp @@ -1,296 +1,299 @@ /* 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 "extractorengine.h" #include "extractor.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; namespace KItinerary { class ContextObject; class ExtractorEnginePrivate { public: void executeScript(); void extractPass(); const Extractor *m_extractor = nullptr; ContextObject *m_context = nullptr; QString m_text; KPkPass::BoardingPass *m_pass; QJsonArray m_result; }; class JsApi : public QObject { Q_OBJECT public: explicit JsApi(QJSEngine *engine) : QObject(engine) , m_engine(engine) { } Q_INVOKABLE QJSValue newObject(const QString &typeName) const; Q_INVOKABLE QDateTime toDateTime(const QString &dtStr, const QString &format, const QString &localeName) const; private: QJSEngine *m_engine; }; QJSValue JsApi::newObject(const QString &typeName) const { auto v = m_engine->newObject(); v.setProperty(QStringLiteral("@type"), typeName); return v; } QDateTime JsApi::toDateTime(const QString &dtStr, const QString &format, const QString &localeName) const { QLocale locale(localeName); const auto dt = locale.toDateTime(dtStr, format); if (dt.isValid()) { return dt; } // try harder for the "MMM" month format // QLocale expects the exact string in QLocale::shortMonthName(), while we often encounter a three // letter month identifier. For en_US that's the same, for Swedish it isn't though for example. So // let's try to fix up the month identifiers to the full short name. if (format.contains(QLatin1String("MMM"))) { auto dtStrFixed = dtStr; for (int i = 0; i < 12; ++i) { const auto monthName = locale.monthName(i, QLocale::ShortFormat); dtStrFixed = dtStrFixed.replace(monthName.left(3), monthName); } return locale.toDateTime(dtStrFixed, format); } return dt; } class ContextObject : public QObject { Q_OBJECT Q_PROPERTY(QDateTime senderDate MEMBER m_senderDate) public: QDateTime m_senderDate; }; } ExtractorEngine::ExtractorEngine() : d(new ExtractorEnginePrivate) { d->m_context = new ContextObject; // will be deleted by QJSEngine taking ownership } ExtractorEngine::ExtractorEngine(ExtractorEngine &&) = default; ExtractorEngine::~ExtractorEngine() = default; void ExtractorEngine::setExtractor(const Extractor *extractor) { d->m_extractor = extractor; } void ExtractorEngine::setText(const QString &text) { d->m_text = text; } void ExtractorEngine::setPass(KPkPass::Pass *pass) { d->m_pass = qobject_cast(pass); } void ExtractorEngine::setSenderDate(const QDateTime &dt) { d->m_context->m_senderDate = dt; } QJsonArray ExtractorEngine::extract() { if (!d->m_extractor) { return {}; } switch (d->m_extractor->type()) { case Extractor::Text: if (d->m_text.isEmpty()) { return {}; } d->executeScript(); break; case Extractor::PkPass: if (!d->m_pass) { return {}; } d->executeScript(); d->extractPass(); break; } return d->m_result; } void ExtractorEnginePrivate::executeScript() { Q_ASSERT(m_extractor); if (m_extractor->scriptFileName().isEmpty()) { return; } QFile f(m_extractor->scriptFileName()); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Failed to open extractor script" << f.fileName() << f.errorString(); return; } QJSEngine engine; engine.installExtensions(QJSEngine::ConsoleExtension); auto jsApi = new JsApi(&engine); engine.globalObject().setProperty(QStringLiteral("JsonLd"), engine.newQObject(jsApi)); engine.globalObject().setProperty(QStringLiteral("Context"), engine.newQObject(m_context)); auto result = engine.evaluate(QString::fromUtf8(f.readAll()), f.fileName()); if (result.isError()) { qCWarning(Log) << "Script parsing error in" << result.property(QLatin1String("fileName")).toString() << ':' << result.property(QLatin1String("lineNumber")).toInt() << result.toString(); return; } auto mainFunc = engine.globalObject().property(QLatin1String("main")); if (!mainFunc.isCallable()) { qCWarning(Log) << "Script has no main() function!"; return; } QJSValueList args; switch (m_extractor->type()) { case Extractor::Text: args = {m_text}; break; case Extractor::PkPass: args = {engine.toScriptValue(m_pass)}; break; } result = mainFunc.call(args); if (result.isError()) { qCWarning(Log) << "Script execution error in" << result.property(QLatin1String("fileName")).toString() << ':' << result.property(QLatin1String("lineNumber")).toInt() << result.toString(); return; } if (result.isArray()) { m_result = QJsonArray::fromVariantList(result.toVariant().toList()); } else if (result.isObject()) { m_result = { QJsonValue::fromVariant(result.toVariant()) }; } else { qCWarning(Log) << "Invalid result type from script"; } } void ExtractorEnginePrivate::extractPass() { if (m_result.size() > 1) { // a pkpass file contains exactly one boarding pass return; } if (m_result.isEmpty()) { // no script run, so we need to create the top-level element ourselves QJsonObject res; QJsonObject resFor; switch (m_pass->transitType()) { case KPkPass::BoardingPass::Air: res.insert(QLatin1String("@type"), QLatin1String("FlightReservation")); resFor.insert(QLatin1String("@type"), QLatin1String("Flight")); break; // TODO expand once we have test files for train tickets default: return; } res.insert(QLatin1String("reservationFor"), resFor); m_result.push_back(res); } // extract structured data from a pkpass, if the extractor script hasn't done so already auto res = m_result.at(0).toObject(); auto resFor = res.value(QLatin1String("reservationFor")).toObject(); // "relevantDate" is the best guess for the boarding time if (m_pass->relevantDate().isValid() && !resFor.contains(QLatin1String("boardingTime"))) { resFor.insert(QLatin1String("boardingTime"), m_pass->relevantDate().toString(Qt::ISODate)); } // location is the best guess for the departure airport geo coordinates auto depAirport = resFor.value(QLatin1String("departureAirport")).toObject(); if (depAirport.isEmpty()) { depAirport.insert(QLatin1String("@type"), QLatin1String("Airport")); } auto depGeo = depAirport.value(QLatin1String("geo")).toObject(); if (m_pass->locations().size() == 1 && depGeo.isEmpty()) { const auto loc = m_pass->locations().at(0); depGeo.insert(QLatin1String("@type"), QLatin1String("GeoCoordinates")); depGeo.insert(QLatin1String("latitude"), loc.latitude()); depGeo.insert(QLatin1String("longitude"), loc.longitude()); depAirport.insert(QLatin1String("geo"), depGeo); resFor.insert(QLatin1String("departureAirport"), depAirport); } // barcode contains the ticket token - if (!m_pass->barcodes().isEmpty() && !res.contains(QLatin1String("ticketToken"))) { + if (!m_pass->barcodes().isEmpty() && !res.contains(QLatin1String("reservedTicket"))) { const auto barcode = m_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(); - res.insert(QLatin1String("ticketToken"), token); + QJsonObject ticket; + ticket.insert(QLatin1String("@type"), QLatin1String("Ticket")); + ticket.insert(QLatin1String("ticketToken"), token); + res.insert(QLatin1String("reservedTicket"), ticket); } res.insert(QLatin1String("reservationFor"), resFor); // associate the pass with the result, so we can find the pass again for display if (!m_pass->passTypeIdentifier().isEmpty() && !m_pass->serialNumber().isEmpty()) { res.insert(QLatin1String("pkpassPassTypeIdentifier"), m_pass->passTypeIdentifier()); res.insert(QLatin1String("pkpassSerialNumber"), m_pass->serialNumber()); } m_result[0] = res; } #include "extractorengine.moc" diff --git a/src/extractorpostprocessor.cpp b/src/extractorpostprocessor.cpp index 11a7f3b..7527593 100644 --- a/src/extractorpostprocessor.cpp +++ b/src/extractorpostprocessor.cpp @@ -1,300 +1,287 @@ /* 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 "extractorpostprocessor.h" #include "calendarhandler.h" #include "jsonlddocument.h" #include "airportdb/airportdb.h" #include "iatabcbpparser.h" -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include using namespace KItinerary; namespace KItinerary { class ExtractorPostprocessorPrivate { public: QVariant processProperty(QVariant obj, const char *name, QVariant (ExtractorPostprocessorPrivate::*processor)(QVariant) const) const; - QVariant processFlightReservation(QVariant res) const; + QVariant processFlightReservation(FlightReservation res) const; QVariant processFlight(QVariant flight) const; QVariant processAirport(QVariant airport) const; QVariant processAirline(QVariant airline) const; void processFlightTime(QVariant &flight, const char *timePropName, const char *airportPropName) const; QVariant processReservation(QVariant res) const; bool filterReservation(const QVariant &res) const; bool filterLodgingReservation(const QVariant &res) const; bool filterFlight(const Flight &flight) const; bool filterAirport(const Airport &airport) const; bool filterTrainOrBusTrip(const QVariant &trip) const; bool filterTrainOrBusStation(const QVariant &station) const; QVector m_data; QDateTime m_contextDate; }; } ExtractorPostprocessor::ExtractorPostprocessor() : d(new ExtractorPostprocessorPrivate) { } ExtractorPostprocessor::ExtractorPostprocessor(ExtractorPostprocessor &&) = default; ExtractorPostprocessor::~ExtractorPostprocessor() = default; void ExtractorPostprocessor::process(const QVector &data) { d->m_data.reserve(data.size()); for (auto elem : data) { if (elem.userType() == qMetaTypeId()) { - elem = d->processFlightReservation(elem); + elem = d->processFlightReservation(elem.value()); } else if (elem.userType() == qMetaTypeId()) { elem = d->processReservation(elem); } else if (elem.userType() == qMetaTypeId()) { elem = d->processReservation(elem); } else if (elem.userType() == qMetaTypeId()) { elem = d->processReservation(elem); } if (d->filterReservation(elem)) { d->m_data.push_back(elem); } } std::stable_sort(d->m_data.begin(), d->m_data.end(), [](const QVariant &lhs, const QVariant &rhs) { return CalendarHandler::startDateTime(lhs) < CalendarHandler::startDateTime(rhs); }); } QVector ExtractorPostprocessor::result() const { return d->m_data; } void ExtractorPostprocessor::setContextDate(const QDateTime& dt) { d->m_contextDate = dt; } QVariant ExtractorPostprocessorPrivate::processProperty(QVariant obj, const char *name, QVariant (ExtractorPostprocessorPrivate::*processor)(QVariant) const) const { auto value = JsonLdDocument::readProperty(obj, name); value = (this->*processor)(value); JsonLdDocument::writeProperty(obj, name, value); return obj; } -QVariant ExtractorPostprocessorPrivate::processFlightReservation(QVariant res) const +QVariant ExtractorPostprocessorPrivate::processFlightReservation(FlightReservation res) const { // expand ticketToken for IATA BCBP data - auto bcbp = JsonLdDocument::readProperty(res, "ticketToken").toString(); + auto bcbp = res.reservedTicket().value().ticketToken(); if (!bcbp.isEmpty()) { if (bcbp.startsWith(QLatin1String("aztecCode:"))) { bcbp = bcbp.mid(10); } else if (bcbp.startsWith(QLatin1String("qrCode:"))) { bcbp = bcbp.mid(7); } const auto bcbpData = IataBcbpParser::parse(bcbp, m_contextDate.date()); if (bcbpData.size() == 1) { - res = JsonLdDocument::apply(bcbpData.at(0), res); + res = JsonLdDocument::apply(bcbpData.at(0), QVariant::fromValue(res)).value(); } } - res = processReservation(res); - res = processProperty(res, "reservationFor", &ExtractorPostprocessorPrivate::processFlight); - return res; + res = processReservation(QVariant::fromValue(res)).value(); + res = processProperty(QVariant::fromValue(res), "reservationFor", &ExtractorPostprocessorPrivate::processFlight).value(); + return QVariant::fromValue(res); } QVariant ExtractorPostprocessorPrivate::processFlight(QVariant flight) const { flight = processProperty(flight, "departureAirport", &ExtractorPostprocessorPrivate::processAirport); flight = processProperty(flight, "arrivalAirport", &ExtractorPostprocessorPrivate::processAirport); flight = processProperty(flight, "airline", &ExtractorPostprocessorPrivate::processAirline); processFlightTime(flight, "boardingTime", "departureAirport"); processFlightTime(flight, "departureTime", "departureAirport"); processFlightTime(flight, "arrivalTime", "arrivalAirport"); return flight; } QVariant ExtractorPostprocessorPrivate::processAirport(QVariant airport) const { // clean up name const auto name = JsonLdDocument::readProperty(airport, "name").toString(); JsonLdDocument::writeProperty(airport, "name", name.trimmed()); // complete missing IATA codes auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (iataCode.isEmpty()) { iataCode = AirportDb::iataCodeFromName(name).toString(); if (!iataCode.isEmpty()) { JsonLdDocument::writeProperty(airport, "iataCode", iataCode); } } // complete missing geo coordinates auto geo = JsonLdDocument::readProperty(airport, "geo"); if (!geo.value().isValid()) { const auto coord = AirportDb::coordinateForAirport(AirportDb::IataCode{iataCode}); if (coord.isValid()) { geo = QVariant::fromValue(GeoCoordinates()); JsonLdDocument::writeProperty(geo, "latitude", coord.latitude); JsonLdDocument::writeProperty(geo, "longitude", coord.longitude); JsonLdDocument::writeProperty(airport, "geo", geo); } } return airport; } QVariant ExtractorPostprocessorPrivate::processAirline(QVariant airline) const { const auto name = JsonLdDocument::readProperty(airline, "name").toString(); JsonLdDocument::writeProperty(airline, "name", name.trimmed()); return airline; } void ExtractorPostprocessorPrivate::processFlightTime(QVariant &flight, const char *timePropName, const char *airportPropName) const { const auto airport = JsonLdDocument::readProperty(flight, airportPropName); const auto iataCode = JsonLdDocument::readProperty(airport, "iataCode").toString(); if (iataCode.isEmpty()) { return; } auto dt = JsonLdDocument::readProperty(flight, timePropName).toDateTime(); if (!dt.isValid() || dt.timeSpec() == Qt::TimeZone) { return; } const auto tz = AirportDb::timezoneForAirport(AirportDb::IataCode{iataCode}); if (!tz.isValid()) { return; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return; } dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); // if we updated from UTC offset to timezone spec here, QDateTime will compare equal // and the auto-generated property code will not actually update the property // so, clear the property first to force an update JsonLdDocument::writeProperty(flight, timePropName, QDateTime()); JsonLdDocument::writeProperty(flight, timePropName, dt); } QVariant ExtractorPostprocessorPrivate::processReservation(QVariant res) const { const auto viewUrl = JsonLdDocument::readProperty(res, "url").toUrl(); const auto modUrl = JsonLdDocument::readProperty(res, "modifyReservationUrl").toUrl(); const auto cancelUrl = JsonLdDocument::readProperty(res, "cancelReservationUrl").toUrl(); // remove duplicated urls if (modUrl.isValid() && viewUrl == modUrl) { JsonLdDocument::removeProperty(res, "modifyReservationUrl"); } if (cancelUrl.isValid() && viewUrl == cancelUrl) { JsonLdDocument::removeProperty(res, "cancelReservationUrl"); } - // move ticketToken to Ticket (Google vs. schema.org difference) - const auto token = JsonLdDocument::readProperty(res, "ticketToken").toString(); - if (!token.isEmpty()) { - auto ticket = JsonLdDocument::readProperty(res, "reservedTicket"); - if (ticket.isNull()) { - ticket = QVariant::fromValue(Ticket{}); - } - if (JsonLdDocument::readProperty(ticket, "ticketToken").toString().isEmpty()) { - JsonLdDocument::writeProperty(ticket, "ticketToken", token); - JsonLdDocument::writeProperty(res, "reservedTicket", ticket); - } - } - return res; } bool ExtractorPostprocessorPrivate::filterReservation(const QVariant &res) const { const auto resFor = JsonLdDocument::readProperty(res, "reservationFor"); if (resFor.isNull()) { return false; } if (resFor.userType() == qMetaTypeId()) { return filterFlight(resFor.value()); } else if (resFor.userType() == qMetaTypeId()) { return filterTrainOrBusTrip(resFor); } else if (resFor.userType() == qMetaTypeId()) { return filterTrainOrBusTrip(resFor); } if (res.userType() == qMetaTypeId()) { return filterLodgingReservation(res); } return true; } bool ExtractorPostprocessorPrivate::filterLodgingReservation(const QVariant &res) const { const auto checkinDate = JsonLdDocument::readProperty(res, "checkinDate").toDateTime(); const auto checkoutDate = JsonLdDocument::readProperty(res, "checkoutDate").toDateTime(); return checkinDate.isValid() && checkoutDate.isValid(); } bool ExtractorPostprocessorPrivate::filterFlight(const Flight &flight) const { // this will be valid if either boarding time, departure time or departure day is set const auto validDate = flight.departureDay().isValid(); return filterAirport(flight.departureAirport()) && filterAirport(flight.arrivalAirport()) && validDate; } bool ExtractorPostprocessorPrivate::filterAirport(const Airport &airport) const { return !airport.iataCode().isEmpty() || !airport.name().isEmpty(); } bool ExtractorPostprocessorPrivate::filterTrainOrBusTrip(const QVariant &trip) const { const auto depDt = JsonLdDocument::readProperty(trip, "departureTime").toDateTime(); const auto arrDt = JsonLdDocument::readProperty(trip, "arrivalTime").toDateTime(); return filterTrainOrBusStation(JsonLdDocument::readProperty(trip, "departureStation")) && filterTrainOrBusStation(JsonLdDocument::readProperty(trip, "arrivalStation")) && depDt.isValid() && arrDt.isValid(); } bool ExtractorPostprocessorPrivate::filterTrainOrBusStation(const QVariant &station) const { return !JsonLdDocument::readProperty(station, "name").toString().isEmpty(); } diff --git a/src/jsonlddocument.cpp b/src/jsonlddocument.cpp index eafd4f4..0496121 100644 --- a/src/jsonlddocument.cpp +++ b/src/jsonlddocument.cpp @@ -1,312 +1,313 @@ /* 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 using namespace KItinerary; static QVariant createInstance(const QJsonObject &obj); // Eurowings workarounds... static const char *fallbackDateTimePattern[] = { "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "MM-dd-yyyy HH:mm" // yes, seriously ;( }; static const auto fallbackDateTimePatternCount = sizeof(fallbackDateTimePattern) / sizeof(const char *); 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])); } if (dt.isNull()) { qCDebug(Log) << "Datetime parsing failed for" << str; } } return dt; } case QVariant::Double: return v.toDouble(); case QVariant::Url: return QUrl(v.toString()); default: break; } if (prop.type() == qMetaTypeId()) { return v.toDouble(); } return createInstance(v.toObject()); } template static QVariant createInstance(const QJsonObject &obj) { T t; for (auto it = obj.begin(); it != obj.end(); ++it) { if (it.key().startsWith(QLatin1Char('@'))) { continue; } const auto idx = T::staticMetaObject.indexOfProperty(it.key().toLatin1().constData()); if (idx < 0) { qCDebug(Log) << "property" << it.key() << "could not be set on object of type" << T::staticMetaObject.className(); continue; } const auto prop = T::staticMetaObject.property(idx); const auto value = propertyValue(prop, it.value()); prop.writeOnGadget(&t, value); } 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(GeoCoordinates); MAKE_FACTORY(Airline); MAKE_FACTORY(Airport); MAKE_FACTORY(FlightReservation); MAKE_FACTORY(Flight); MAKE_FACTORY(LodgingBusiness); MAKE_FACTORY(LodgingReservation); MAKE_FACTORY(FoodEstablishment); MAKE_FACTORY(TouristAttraction); MAKE_FACTORY(PostalAddress); MAKE_FACTORY(Seat); MAKE_FACTORY(Ticket); MAKE_FACTORY(TrainStation); MAKE_FACTORY(TrainTrip); MAKE_FACTORY(TrainReservation); MAKE_FACTORY(BusStation); MAKE_FACTORY(BusTrip); MAKE_FACTORY(BusReservation); MAKE_FACTORY(Organization); return {}; } #undef MAKE_FACTORY QVector JsonLdDocument::fromJson(const QJsonArray &array) { QVector l; l.reserve(array.size()); for (const auto &obj : array) { - const auto v = createInstance(obj.toObject()); + const auto v = createInstance(JsonLdImportFilter::filterObject(obj.toObject())); if (!v.isNull()) { l.push_back(v); } } return l; } 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 QJsonValue toJson(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(); default: break; } if (v.userType() == qMetaTypeId()) { return v.toFloat(); } qCDebug(Log) << "unhandled value:" << v; return {}; } // composite types QJsonObject obj; obj.insert(QStringLiteral("@type"), JsonLdDocument::readProperty(v, "className").toString()); for (int i = 0; i < mo->propertyCount(); ++i) { const auto prop = mo->property(i); if (!prop.isStored()) { continue; } const auto value = prop.readOnGadget(v.constData()); if (!valueIsNull(value)) { const auto jsVal = toJson(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 = toJson(d); if (!value.isObject()) { continue; } auto obj = value.toObject(); obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org")); a.push_back(obj); } return a; } 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; } const auto idx = mo->indexOfProperty(name); if (idx < 0) { return; } const auto prop = mo->property(idx); prop.writeOnGadget(obj.data(), 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; } 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 5a2c4cd..1102529 100644 --- a/src/jsonlddocument.h +++ b/src/jsonlddocument.h @@ -1,53 +1,57 @@ /* 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; namespace KItinerary { -/** Serialization/deserialization code for JSON-LD data. */ +/** Serialization/deserialization code for JSON-LD data. + * @see https://www.w3.org/TR/json-ld/ + */ namespace JsonLdDocument { +/** Convert JSON-LD data into instantiated data types. */ KITINERARY_EXPORT QVector fromJson(const QJsonArray &array); +/** Serialize instantiated data types to JSON. */ KITINERARY_EXPORT QJsonArray toJson(const QVector &data); /** Read property @p name on object @p obj. */ KITINERARY_EXPORT QVariant readProperty(const QVariant &obj, const char *name); /** Set property @p name on object @p obj to value @p value. */ KITINERARY_EXPORT void writeProperty(QVariant &obj, const char *name, const QVariant &value); /** Removes property @p name on object @p obj. */ KITINERARY_EXPORT 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 QVariant apply(const QVariant &lhs, const QVariant &rhs); } } #endif // KITINERARY_JSONLDDOCUMENT_H diff --git a/src/jsonldimportfilter.cpp b/src/jsonldimportfilter.cpp new file mode 100644 index 0000000..701999e --- /dev/null +++ b/src/jsonldimportfilter.cpp @@ -0,0 +1,48 @@ +/* + 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 + +using namespace KItinerary; + +static QJsonObject 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(QLatin1String("@type"), QLatin1String("Ticket")); + } + if (!ticket.contains(QLatin1String("ticketToken"))) { + ticket.insert(QLatin1String("ticketToken"), token); + res.insert(QLatin1String("reservedTicket"), ticket); + } + } + + return res; +} + +QJsonObject JsonLdImportFilter::filterObject(const QJsonObject& obj) +{ + if (obj.value(QLatin1String("@type")).toString().endsWith(QLatin1String("Reservation"))) { + return filterReservation(obj); + } + return obj; +} diff --git a/src/jsonldimportfilter.h b/src/jsonldimportfilter.h new file mode 100644 index 0000000..c64aa83 --- /dev/null +++ b/src/jsonldimportfilter.h @@ -0,0 +1,35 @@ +/* + 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 QJsonObject; + +namespace KItinerary { + +/** Filter input JSON for loading with JsonLdDocument, to deal with obsolete + * or renamed elements. + */ +namespace JsonLdImportFilter +{ + QJsonObject filterObject(const QJsonObject &obj); +} + +} + +#endif // KITINERARY_JSONLDIMPORTFILTER_H