diff --git a/autotests/rct2/valid/irt-oebb.json b/autotests/rct2/valid/irt-oebb.json index 57260f4..ee65370 100644 --- a/autotests/rct2/valid/irt-oebb.json +++ b/autotests/rct2/valid/irt-oebb.json @@ -1,17 +1,17 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "coachNumber": "280", "firstDayOfValidity": "2018-01-01", "outboundArrivalStation": "NUERNBERG", "outboundArrivalTime": "2018-12-29T02:37:00", "outboundClass": "2", "outboundDepartureStation": "SOMEWHERE HBF", "outboundDepartureTime": "2018-12-29T01:37:00", "passengerName": "Lastname Firs", "seatNumber": "FENSTER 046", "trainNumber": "NJ 421", - "type": 1 + "type": "TransportReservation" } ] diff --git a/autotests/rct2/valid/nrt-db.json b/autotests/rct2/valid/nrt-db.json index e8431db..b836e02 100644 --- a/autotests/rct2/valid/nrt-db.json +++ b/autotests/rct2/valid/nrt-db.json @@ -1,12 +1,12 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "firstDayOfValidity": "2018-08-10", "outboundArrivalStation": "Wien", "outboundClass": "2", "outboundDepartureStation": "Duisburg", "passengerName": "Last Name Firstn", - "type": 0 + "type": "Transport" } ] diff --git a/autotests/rct2/valid/nrt-oebb-bound.json b/autotests/rct2/valid/nrt-oebb-bound.json index a8736ee..10555da 100644 --- a/autotests/rct2/valid/nrt-oebb-bound.json +++ b/autotests/rct2/valid/nrt-oebb-bound.json @@ -1,14 +1,14 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "firstDayOfValidity": "2017-01-01", "outboundArrivalStation": "GRAZ HBF", "outboundArrivalTime": "2017-09-16T21:33:00", "outboundClass": "1", "outboundDepartureStation": "WIEN HBF", "outboundDepartureTime": "2017-09-16T18:58:00", "passengerName": "Lastname Name", - "type": 0 + "type": "Transport" } ] diff --git a/autotests/rct2/valid/nrt-oebb-unbound.json b/autotests/rct2/valid/nrt-oebb-unbound.json index 61201c3..df1ddd9 100644 --- a/autotests/rct2/valid/nrt-oebb-unbound.json +++ b/autotests/rct2/valid/nrt-oebb-unbound.json @@ -1,12 +1,12 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "firstDayOfValidity": "2017-09-10", "outboundArrivalStation": "WIEN", "outboundClass": "1", "outboundDepartureStation": "GRAZ", "passengerName": "Lastname Name", - "type": 0 + "type": "Transport" } ] diff --git a/autotests/rct2/valid/nrt-sbb.json b/autotests/rct2/valid/nrt-sbb.json index 55d9e0a..0c35d10 100644 --- a/autotests/rct2/valid/nrt-sbb.json +++ b/autotests/rct2/valid/nrt-sbb.json @@ -1,14 +1,14 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "firstDayOfValidity": "2012-01-01", "outboundArrivalStation": "RANDA", "outboundArrivalTime": "2012-09-21T00:00:00", "outboundClass": "2", "outboundDepartureStation": "VISP", "outboundDepartureTime": "2012-09-21T00:00:00", "passengerName": "Last Name First Nam", - "type": 0 + "type": "Transport" } ] diff --git a/autotests/rct2/valid/res-dsb-invalid-unicode-encoding.json b/autotests/rct2/valid/res-dsb-invalid-unicode-encoding.json index 547f67c..2b6c065 100644 --- a/autotests/rct2/valid/res-dsb-invalid-unicode-encoding.json +++ b/autotests/rct2/valid/res-dsb-invalid-unicode-encoding.json @@ -1,15 +1,15 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "coachNumber": "062", "outboundArrivalStation": "Odense", "outboundArrivalTime": "2018-12-08T18:50:00", "outboundClass": "2", "outboundDepartureStation": "København", "outboundDepartureTime": "2018-12-08T17:22:00", "seatNumber": "103", "trainNumber": "267", - "type": 2 + "type": "Reservation" } ] diff --git a/autotests/rct2/valid/res-oebb.json b/autotests/rct2/valid/res-oebb.json index 058782f..553f169 100644 --- a/autotests/rct2/valid/res-oebb.json +++ b/autotests/rct2/valid/res-oebb.json @@ -1,16 +1,16 @@ [ { "@context": "http://schema.org", "@type": "Rct2Ticket", "coachNumber": "256", "firstDayOfValidity": "2010-01-01", "outboundArrivalStation": "MUENCHEN HBF", "outboundArrivalTime": "2010-05-16T16:25:00", "outboundClass": "2", "outboundDepartureStation": "VERONA PN", "outboundDepartureTime": "2010-05-16T11:02:00", "seatNumber": "FENSTER 023", "trainNumber": "EC 42", - "type": 2 + "type": "Reservation" } ] diff --git a/src/jsonlddocument.cpp b/src/jsonlddocument.cpp index a1c021c..8cf27e3 100644 --- a/src/jsonlddocument.cpp +++ b/src/jsonlddocument.cpp @@ -1,435 +1,478 @@ /* 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(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); 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)); } 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; }