jsonlddocument.cpp 15.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
   Copyright (c) 2017 Volker Krause <vkrause@kde.org>

   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"
21
#include "jsonldimportfilter.h"
Volker Krause's avatar
Volker Krause committed
22
#include "logging.h"
23

24
#include <KItinerary/Action>
Benjamin Port's avatar
Benjamin Port committed
25
#include <KItinerary/Brand>
26
#include <KItinerary/BusTrip>
27
#include <KItinerary/CreativeWork>
28
#include <KItinerary/Event>
29
30
31
32
33
#include <KItinerary/Flight>
#include <KItinerary/Organization>
#include <KItinerary/Person>
#include <KItinerary/Place>
#include <KItinerary/Reservation>
Laurent Montel's avatar
Laurent Montel committed
34
#include <KItinerary/RentalCar>
Laurent Montel's avatar
Laurent Montel committed
35
#include <KItinerary/Taxi>
36
37
#include <KItinerary/Ticket>
#include <KItinerary/TrainTrip>
38
#include <KItinerary/Visit>
Volker Krause's avatar
Volker Krause committed
39
40

#include <QDateTime>
41
42
43
#include <QJsonArray>
#include <QJsonObject>
#include <QMetaProperty>
Volker Krause's avatar
Volker Krause committed
44
#include <QUrl>
45
#include <QTimeZone>
46
#include <QVector>
Volker Krause's avatar
Volker Krause committed
47

48
49
#include <cmath>

Volker Krause's avatar
Volker Krause committed
50
using namespace KItinerary;
51
52
53
54

static QVariant createInstance(const QJsonObject &obj);

// Eurowings workarounds...
55
static const char* const fallbackDateTimePattern[] = {
56
57
    "yyyy-MM-dd HH:mm:ss",
    "yyyy-MM-dd HH:mm",
58
59
    "MM-dd-yyyy HH:mm", // yes, seriously ;(
    "yyyyMMddTHHmmsst"
60
};
Laurent Montel's avatar
Laurent Montel committed
61
static const auto fallbackDateTimePatternCount = sizeof(fallbackDateTimePattern) / sizeof(const char *);
62

63
64
65
66
67
68
69
70
static double doubleValue(const QJsonValue &v)
{
    if (v.isDouble()) {
        return v.toDouble();
    }
    return v.toString().toDouble();
}

71
72
73
static QVariant propertyValue(const QMetaProperty &prop, const QJsonValue &v)
{
    switch (prop.type()) {
Laurent Montel's avatar
Laurent Montel committed
74
75
    case QVariant::String:
        return v.toString();
Volker Krause's avatar
Volker Krause committed
76
77
    case QVariant::Date:
        return QDate::fromString(v.toString(), Qt::ISODate);
Laurent Montel's avatar
Laurent Montel committed
78
79
    case QVariant::DateTime:
    {
80
81
82
83
84
85
86
87
88
89
90
91
92
        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]));
            }
93
94
            // 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'))) {
95
                dt = dt.toTimeSpec(Qt::UTC);
96
            }
97
            if (dt.isNull()) {
Volker Krause's avatar
Volker Krause committed
98
                qCDebug(Log) << "Datetime parsing failed for" << str;
99
            }
Laurent Montel's avatar
Laurent Montel committed
100
        }
101

Laurent Montel's avatar
Laurent Montel committed
102
103
        return dt;
    }
104
    case QVariant::Double:
105
        return doubleValue(v);
106
    case QVariant::Int:
107
        if (v.isDouble()) {
108
            return v.toDouble();
109
        }
110
        return v.toString().toInt();
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
111
112
    case QVariant::Time:
        return QTime::fromString(v.toString(), Qt::ISODate);
113
114
    case QVariant::Url:
        return QUrl(v.toString());
Laurent Montel's avatar
Laurent Montel committed
115
116
    default:
        break;
117
    }
Laurent Montel's avatar
Laurent Montel committed
118
    if (prop.type() == qMetaTypeId<float>()) {
119
        return doubleValue(v);
Laurent Montel's avatar
Laurent Montel committed
120
    }
121

122
123
    if (prop.userType() == qMetaTypeId<QVariantList>()) {
        QVariantList l;
124
125
126
127
        if (v.isArray()) {
            const auto array = v.toArray();
            l.reserve(array.size());
            for (const auto &elem : array) {
Volker Krause's avatar
Volker Krause committed
128
129
130
131
132
133
134
                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());
135
136
137
138
139
140
                }
            }
        }
        return QVariant::fromValue(l);
    }

141
142
143
    return createInstance(v.toObject());
}

Volker Krause's avatar
Volker Krause committed
144
static void createInstance(const QMetaObject *mo, void *v, const QJsonObject &obj)
145
{
Volker Krause's avatar
Volker Krause committed
146
   for (auto it = obj.begin(); it != obj.end(); ++it) {
Laurent Montel's avatar
Laurent Montel committed
147
        if (it.key().startsWith(QLatin1Char('@'))) {
148
            continue;
Laurent Montel's avatar
Laurent Montel committed
149
        }
Volker Krause's avatar
Volker Krause committed
150
        const auto idx = mo->indexOfProperty(it.key().toLatin1().constData());
151
        if (idx < 0) {
Volker Krause's avatar
Volker Krause committed
152
            qCDebug(Log) << "property" << it.key() << "could not be set on object of type" << mo->className();
153
154
            continue;
        }
Volker Krause's avatar
Volker Krause committed
155
        const auto prop = mo->property(idx);
156
        const auto value = propertyValue(prop, it.value());
Volker Krause's avatar
Volker Krause committed
157
        prop.writeOnGadget(v, value);
158
    }
Volker Krause's avatar
Volker Krause committed
159
160
161
162
163
164
165
}

template<typename T>
static QVariant createInstance(const QJsonObject &obj)
{
    T t;
    createInstance(&T::staticMetaObject, &t, obj);
166
167
168
169
170
171
172
173
174
175
    return QVariant::fromValue(t);
}

#define MAKE_FACTORY(Class) \
    if (type == QLatin1String(#Class)) \
        return createInstance<Class>(obj)

static QVariant createInstance(const QJsonObject &obj)
{
    const auto type = obj.value(QLatin1String("@type")).toString();
176
    MAKE_FACTORY(Action);
177
178
    MAKE_FACTORY(Airline);
    MAKE_FACTORY(Airport);
Benjamin Port's avatar
Benjamin Port committed
179
    MAKE_FACTORY(Brand);
180
181
182
    MAKE_FACTORY(BusReservation);
    MAKE_FACTORY(BusStation);
    MAKE_FACTORY(BusTrip);
183
184
    MAKE_FACTORY(CancelAction);
    MAKE_FACTORY(CheckInAction);
185
186
    MAKE_FACTORY(CreativeWork);
    MAKE_FACTORY(DigitalDocument);
187
    MAKE_FACTORY(DownloadAction);
188
    MAKE_FACTORY(EmailMessage);
189
190
    MAKE_FACTORY(Event);
    MAKE_FACTORY(EventReservation);
191
    MAKE_FACTORY(Flight);
192
193
    MAKE_FACTORY(FlightReservation);
    MAKE_FACTORY(FoodEstablishment);
194
    MAKE_FACTORY(FoodEstablishmentReservation);
195
    MAKE_FACTORY(LocalBusiness);
196
    MAKE_FACTORY(RentalCarReservation);
Laurent Montel's avatar
Laurent Montel committed
197
    MAKE_FACTORY(RentalCar);
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
198
    MAKE_FACTORY(ReserveAction);
199
    MAKE_FACTORY(GeoCoordinates);
200
201
    MAKE_FACTORY(LodgingBusiness);
    MAKE_FACTORY(LodgingReservation);
202
203
204
    MAKE_FACTORY(Organization);
    MAKE_FACTORY(Person);
    MAKE_FACTORY(Place);
205
    MAKE_FACTORY(PostalAddress);
206
    MAKE_FACTORY(Seat);
207
    MAKE_FACTORY(TaxiReservation);
Laurent Montel's avatar
Laurent Montel committed
208
    MAKE_FACTORY(Taxi);
209
    MAKE_FACTORY(Ticket);
210
    MAKE_FACTORY(TouristAttraction);
211
    MAKE_FACTORY(TouristAttractionVisit);
212
    MAKE_FACTORY(TrainReservation);
213
214
    MAKE_FACTORY(TrainStation);
    MAKE_FACTORY(TrainTrip);
215
    MAKE_FACTORY(UpdateAction);
216
217
218
219
220
221
222
223
    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;
    }

224
225
226
227
    return {};
}

#undef MAKE_FACTORY
228
QVector<QVariant> JsonLdDocument::fromJson(const QJsonArray &array)
229
{
230
231
    QVector<QVariant> l;
    l.reserve(array.size());
232
    for (const auto &obj : array) {
233
        const auto v = fromJson(obj.toObject());
Laurent Montel's avatar
Laurent Montel committed
234
        if (!v.isNull()) {
235
            l.push_back(v);
Laurent Montel's avatar
Laurent Montel committed
236
237
238
        }
    }
    return l;
239
}
240

241
242
QVariant JsonLdDocument::fromJson(const QJsonObject& obj)
{
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    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());
258
259
}

260
261
262
263
264
static bool valueIsNull(const QVariant &v)
{
    if (v.type() == QVariant::Url) {
        return !v.toUrl().isValid();
    }
265
266
267
    if (v.type() == qMetaTypeId<float>()) {
        return std::isnan(v.toFloat());
    }
268
269
270
    return v.isNull();
}

271
272
273
274
275
276
277
278
279
280
281
282
283
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());
}

284
static QJsonValue toJsonValue(const QVariant &v)
285
286
287
288
{
    const auto mo = QMetaType(v.userType()).metaObject();
    if (!mo) {
        // basic types
Laurent Montel's avatar
Laurent Montel committed
289
290
291
292
293
294
295
        switch (v.type()) {
        case QVariant::String:
            return v.toString();
        case QVariant::Double:
            return v.toDouble();
        case QVariant::Int:
            return v.toInt();
Volker Krause's avatar
Volker Krause committed
296
297
        case QVariant::Date:
            return v.toDate().toString(Qt::ISODate);
Laurent Montel's avatar
Laurent Montel committed
298
        case QVariant::DateTime:
299
300
301
302
303
304
305
306
307
        {
            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;
            }
Laurent Montel's avatar
Laurent Montel committed
308
            return v.toDateTime().toString(Qt::ISODate);
309
        }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
310
311
        case QVariant::Time:
            return v.toTime().toString(Qt::ISODate);
312
313
        case QVariant::Url:
            return v.toUrl().toString();
314
315
        case QVariant::Bool:
            return v.toBool();
Laurent Montel's avatar
Laurent Montel committed
316
317
        default:
            break;
318
319
320
321
        }
        if (v.userType() == qMetaTypeId<float>()) {
            return v.toFloat();
        }
322

323
324
325
        if (v.canConvert<QVariantList>()) {
            QSequentialIterable iterable = v.value<QSequentialIterable>();
            if (iterable.size() == 0) {
326
327
328
                return {};
            }
            QJsonArray array;
329
            for (const auto &var : iterable) {
330
                array.push_back(toJsonValue(var));
331
332
333
334
            }
            return array;
        }

Volker Krause's avatar
Volker Krause committed
335
        qCDebug(Log) << "unhandled value:" << v;
336
337
338
339
340
        return {};
    }

    // composite types
    QJsonObject obj;
341
    obj.insert(QStringLiteral("@type"), typeName(mo, v));
342
343
344
345
346
    for (int i = 0; i < mo->propertyCount(); ++i) {
        const auto prop = mo->property(i);
        if (!prop.isStored()) {
            continue;
        }
347
348
349
350
351
352
353
354
355
356
357

        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;
        }

358
        const auto value = prop.readOnGadget(v.constData());
359
        if (!valueIsNull(value)) {
360
            const auto jsVal = toJsonValue(value);
361
            if (jsVal.type() != QJsonValue::Null) {
Volker Krause's avatar
Volker Krause committed
362
                obj.insert(QString::fromUtf8(prop.name()), jsVal);
363
            }
364
365
        }
    }
366
367
368
369
370
    if (obj.size() > 1) {
        return obj;
    }

    return {};
371
372
373
374
375
376
}

QJsonArray JsonLdDocument::toJson(const QVector<QVariant> &data)
{
    QJsonArray a;
    for (const auto &d : data) {
377
        const auto value = ::toJsonValue(d);
378
379
380
381
382
383
384
385
386
387
        if (!value.isObject()) {
            continue;
        }
        auto obj = value.toObject();
        obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org"));
        a.push_back(obj);
    }
    return a;
}

388
389
390
QJsonObject JsonLdDocument::toJson(const QVariant& data)
{
    const auto value = ::toJsonValue(data);
391
392
    if (!value.isObject()) {
        return {};
393
    }
394
395
396
    auto obj = value.toObject();
    obj.insert(QStringLiteral("@context"), QStringLiteral("http://schema.org"));
    return obj;
397
398
}

399
QVariant JsonLdDocument::readProperty(const QVariant &obj, const char *name)
400
{
401
    const auto mo = QMetaType(obj.userType()).metaObject();
402
403
404
405
406
407
408
409
410
411
    if (!mo) {
        return {};
    }

    const auto idx = mo->indexOfProperty(name);
    if (idx < 0) {
        return {};
    }

    const auto prop = mo->property(idx);
412
413
414
415
416
417
418
419
420
421
    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;
    }

422
423
424
425
426
    writePropertyImpl(mo, obj.data(), name, value);
}

void JsonLdDocument::writePropertyImpl(const QMetaObject* mo, void* obj, const char* name, const QVariant& value)
{
427
428
429
430
431
432
    const auto idx = mo->indexOfProperty(name);
    if (idx < 0) {
        return;
    }

    const auto prop = mo->property(idx);
433
    prop.writeOnGadget(obj, value);
434
}
435
436
437
438
439

void JsonLdDocument::removeProperty(QVariant &obj, const char *name)
{
    writeProperty(obj, name, QVariant());
}
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460

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;
        }
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492

        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<int*>(valueData) = numValue;
            QVariant value(prop.userType(), valueData);
            prop.writeOnGadget(res.data(), value);
            continue;
        }

493
494
495
496
497
498
499
500
501
502
503
        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;
}