diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fcdd675..7203975 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,156 +1,158 @@ set(KDE_INSTALL_INCLUDEDIR_PIM ${KDE_INSTALL_INCLUDEDIR}/KPim) add_subdirectory(knowledgedb-generator) configure_file(config-kitinerary.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kitinerary.h) set(kitinerary_lib_srcs datatypes/action.cpp + datatypes/brand.cpp datatypes/bustrip.cpp datatypes/event.cpp datatypes/flight.cpp datatypes/organization.cpp datatypes/person.cpp datatypes/place.cpp datatypes/reservation.cpp datatypes/taxi.cpp datatypes/ticket.cpp datatypes/traintrip.cpp datatypes/rentalcar.cpp datatypes/visit.cpp jsapi/barcode.cpp jsapi/context.cpp jsapi/jsonld.cpp knowledgedb/airportdb.cpp knowledgedb/countrydb.cpp knowledgedb/knowledgedb.cpp knowledgedb/timezonedb.cpp knowledgedb/trainstationdb.cpp barcodedecoder.cpp calendarhandler.cpp extractor.cpp extractorengine.cpp extractorfilter.cpp extractorpreprocessor.cpp extractorpostprocessor.cpp extractorrepository.cpp htmldocument.cpp iatabcbpparser.cpp jsonlddocument.cpp jsonldimportfilter.cpp mergeutil.cpp pdfdocument.cpp sortutil.cpp structureddataextractor.cpp uic9183parser.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) kde_source_files_enable_exceptions(barcodedecoder.cpp) add_library(KPimItinerary ${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 PRIVATE Qt5::Qml KF5::I18n KPim::PkPass ${ZLIB_LIBRARIES} ) if (HAVE_POPPLER) target_link_libraries(KPimItinerary PRIVATE Poppler::Core) endif() if (HAVE_ZXING) target_link_libraries(KPimItinerary PRIVATE zxing::libzxing) endif() if (HAVE_KCAL) target_link_libraries(KPimItinerary PUBLIC KF5::CalendarCore) endif() if (HAVE_KCONTACTS) target_link_libraries(KPimItinerary PRIVATE KF5::Contacts) endif() if (HAVE_LIBXML2) target_compile_definitions(KPimItinerary PRIVATE ${LIBXML2_DEFINITIONS}) target_include_directories(KPimItinerary PRIVATE ${LIBXML2_INCLUDE_DIR}) target_link_libraries(KPimItinerary PRIVATE ${LIBXML2_LIBRARIES}) endif() ecm_generate_headers(KItinerary_FORWARDING_HEADERS HEADER_NAMES BarcodeDecoder CalendarHandler Extractor ExtractorEngine ExtractorPreprocessor ExtractorPostprocessor ExtractorRepository HtmlDocument IataBcbpParser JsonLdDocument MergeUtil PdfDocument SortUtil StructuredDataExtractor Uic9183Parser PREFIX KItinerary REQUIRED_HEADERS KItinerary_HEADERS ) ecm_generate_headers(KItinerary_KnowledgeDb_FORWARDING_HEADERS HEADER_NAMES AirportDb CountryDb KnowledgeDb TrainStationDb PREFIX KItinerary REQUIRED_HEADERS KItinerary_KnowledgeDb_HEADERS RELATIVE knowledgedb ) ecm_generate_headers(KItinerary_Datatypes_FORWARDING_HEADERS HEADER_NAMES Action + Brand BusTrip Datatypes Event Flight Organization Reservation RentalCar Person Place Taxi Ticket TrainTrip Visit PREFIX KItinerary REQUIRED_HEADERS KItinerary_Datatypes_HEADERS RELATIVE datatypes ) install(TARGETS KPimItinerary EXPORT KPimItineraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KItinerary_FORWARDING_HEADERS} ${KItinerary_KnowledgeDb_FORWARDING_HEADERS} ${KItinerary_Datatypes_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary ) install(FILES ${KItinerary_HEADERS} ${KItinerary_AirportDb_HEADERS} ${KItinerary_Datatypes_HEADERS} ${KItinerary_KnowledgeDb_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kitinerary_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/kitinerary ) diff --git a/src/datatypes/rentalcar.cpp b/src/datatypes/brand.cpp similarity index 68% copy from src/datatypes/rentalcar.cpp copy to src/datatypes/brand.cpp index 6ec1e73..674d4f2 100644 --- a/src/datatypes/rentalcar.cpp +++ b/src/datatypes/brand.cpp @@ -1,40 +1,36 @@ /* - Copyright (C) 2018 Laurent Montel + Copyright (C) 2018 Benjamin Port 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 "rentalcar.h" +#include "brand.h" #include "datatypes_p.h" -#include -#include - using namespace KItinerary; namespace KItinerary { -class RentalCarPrivate: public QSharedData { - +class BrandPrivate : public QSharedData +{ public: QString name; - QString model; }; -KITINERARY_MAKE_SIMPLE_CLASS(RentalCar) -KITINERARY_MAKE_PROPERTY(RentalCar, QString, name, setName) -KITINERARY_MAKE_PROPERTY(RentalCar, QString, model, setModel) +KITINERARY_MAKE_SIMPLE_CLASS(Brand) +KITINERARY_MAKE_PROPERTY(Brand, QString, name, setName) + } -#include "moc_rentalcar.cpp" +#include "moc_brand.cpp" diff --git a/src/datatypes/rentalcar.h b/src/datatypes/brand.h similarity index 61% copy from src/datatypes/rentalcar.h copy to src/datatypes/brand.h index c3d509a..583f6aa 100644 --- a/src/datatypes/rentalcar.h +++ b/src/datatypes/brand.h @@ -1,45 +1,44 @@ /* - Copyright (C) 2018 Laurent Montel + Copyright (C) 2018 Benjamin Port 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_RENTALCAR_H -#define KITINERARY_RENTALCAR_H +#ifndef KITINERARY_BRAND_H +#define KITINERARY_BRAND_H #include "kitinerary_export.h" #include "datatypes.h" namespace KItinerary { -class RentalCarPrivate; +class BrandPrivate; -/** An event. - * @see https://developers.google.com/gmail/markup/reference/event-reservation +/** A brand + * @see https://schema.org/Brand */ -class KITINERARY_EXPORT RentalCar +class KITINERARY_EXPORT Brand { - KITINERARY_GADGET(RentalCar) + KITINERARY_GADGET(Brand) KITINERARY_PROPERTY(QString, name, setName) - KITINERARY_PROPERTY(QString, model, setModel) - //Add more info : brand/rentalcompany + private: - QExplicitlySharedDataPointer d; + QExplicitlySharedDataPointer d; }; } -Q_DECLARE_METATYPE(KItinerary::RentalCar) +Q_DECLARE_METATYPE(KItinerary::Brand) -#endif // KITINERARY_RENTALCAR_H +#endif // KITINERARY_BRAND_H diff --git a/src/datatypes/place.cpp b/src/datatypes/place.cpp index 87bdfe3..adcbec5 100644 --- a/src/datatypes/place.cpp +++ b/src/datatypes/place.cpp @@ -1,123 +1,125 @@ /* 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 "place.h" #include "datatypes_p.h" #include using namespace KItinerary; namespace KItinerary { class GeoCoordinatesPrivate : public QSharedData { public: float latitude = NAN; float longitude = NAN; }; KITINERARY_MAKE_SIMPLE_CLASS(GeoCoordinates) KITINERARY_MAKE_PROPERTY(GeoCoordinates, float, latitude, setLatitude) KITINERARY_MAKE_PROPERTY(GeoCoordinates, float, longitude, setLongitude) bool GeoCoordinates::isValid() const { return !std::isnan(d->latitude) && !std::isnan(d->longitude); } class PostalAddressPrivate : public QSharedData { public: QString streetAddress; QString addressLocality; QString postalCode; QString addressRegion; QString addressCountry; }; KITINERARY_MAKE_SIMPLE_CLASS(PostalAddress) KITINERARY_MAKE_PROPERTY(PostalAddress, QString, streetAddress, setStreeAddress) KITINERARY_MAKE_PROPERTY(PostalAddress, QString, addressLocality, setAddressLocality) KITINERARY_MAKE_PROPERTY(PostalAddress, QString, postalCode, setPostalCode) KITINERARY_MAKE_PROPERTY(PostalAddress, QString, addressRegion, setAddressRegion) KITINERARY_MAKE_PROPERTY(PostalAddress, QString, addressCountry, setAddressCountry) bool PostalAddress::isEmpty() const { return d->streetAddress.isEmpty() && d->addressLocality.isEmpty() && d->postalCode.isEmpty() && d->addressRegion.isEmpty() && d->addressCountry.isEmpty(); } class PlacePrivate : public QSharedData { KITINERARY_PRIVATE_BASE_GADGET(Place) public: QString name; PostalAddress address; GeoCoordinates geo; + QString telephone; QString identifier; }; KITINERARY_MAKE_BASE_CLASS(Place) KITINERARY_MAKE_PROPERTY(Place, QString, name, setName) KITINERARY_MAKE_PROPERTY(Place, PostalAddress, address, setAddress) KITINERARY_MAKE_PROPERTY(Place, GeoCoordinates, geo, setGeo) +KITINERARY_MAKE_PROPERTY(Place, QString, telephone, setTelephone) KITINERARY_MAKE_PROPERTY(Place, QString, identifier, setIdentifier) class AirportPrivate : public PlacePrivate { KITINERARY_PRIVATE_GADGET(Airport) public: QString iataCode; }; KITINERARY_MAKE_SUB_CLASS(Airport, Place) KITINERARY_MAKE_PROPERTY(Airport, QString, iataCode, setIataCode) class TrainStationPrivate : public PlacePrivate { KITINERARY_PRIVATE_GADGET(TrainStation) }; KITINERARY_MAKE_SUB_CLASS(TrainStation, Place) class BusStationPrivate : public PlacePrivate { KITINERARY_PRIVATE_GADGET(BusStation) }; KITINERARY_MAKE_SUB_CLASS(BusStation, Place) class TouristAttractionPrivate: public PlacePrivate { KITINERARY_PRIVATE_GADGET(TouristAttraction) }; KITINERARY_MAKE_SUB_CLASS(TouristAttraction, Place) } template <> KItinerary::PlacePrivate *QExplicitlySharedDataPointer::clone() { return d->clone(); } #include "moc_place.cpp" diff --git a/src/datatypes/place.h b/src/datatypes/place.h index 1d07bb0..1053886 100644 --- a/src/datatypes/place.h +++ b/src/datatypes/place.h @@ -1,142 +1,143 @@ /* 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_PLACE_H #define KITINERARY_PLACE_H #include "kitinerary_export.h" #include "datatypes.h" class QVariant; namespace KItinerary { class GeoCoordinatesPrivate; /** Geographic coordinates. * @see https://schema.org/GeoCoordinates */ class KITINERARY_EXPORT GeoCoordinates { KITINERARY_GADGET(GeoCoordinates) KITINERARY_PROPERTY(float, latitude, setLatitude) KITINERARY_PROPERTY(float, longitude, setLongitude) Q_PROPERTY(bool isValid READ isValid STORED false) public: /** Returns @c true if both latitude and longitude are set and within * the valid range. */ bool isValid() const; private: QExplicitlySharedDataPointer d; }; class PostalAddressPrivate; /** Postal address. * @see https://schema.org/PostalAddress */ class KITINERARY_EXPORT PostalAddress { KITINERARY_GADGET(PostalAddress) KITINERARY_PROPERTY(QString, streetAddress, setStreeAddress) KITINERARY_PROPERTY(QString, addressLocality, setAddressLocality) KITINERARY_PROPERTY(QString, postalCode, setPostalCode) KITINERARY_PROPERTY(QString, addressRegion, setAddressRegion) /** The country this address is in, as ISO 3166-1 alpha 2 code. */ KITINERARY_PROPERTY(QString, addressCountry, setAddressCountry) Q_PROPERTY(bool isEmpty READ isEmpty STORED false) public: /** Returns @c true if there is no property set in this object. */ bool isEmpty() const; private: QExplicitlySharedDataPointer d; }; class PlacePrivate; /** Base class for places. * @see https://schema.org/Place */ class KITINERARY_EXPORT Place { KITINERARY_BASE_GADGET(Place) KITINERARY_PROPERTY(QString, name, setName) KITINERARY_PROPERTY(KItinerary::PostalAddress, address, setAddress) KITINERARY_PROPERTY(KItinerary::GeoCoordinates, geo, setGeo) + KITINERARY_PROPERTY(QString, telephone, setTelephone) /** Identifier. * We use the following schemas currently: * - 'uic:', UIC station code (see https://www.wikidata.org/wiki/Property:P722) * - 'sncf:', Gares & Connextions ID, (see https://www.wikidata.org/wiki/Property:P3104), French train station identifier. * - 'ibnr:', Internationale Bahnhofsnummer, (see https://www.wikidata.org/wiki/Property:P954), German train station identifier. * @see http://schema.org/docs/datamodel.html#identifierBg */ KITINERARY_PROPERTY(QString, identifier, setIdentifier) protected: ///@cond internal QExplicitlySharedDataPointer d; ///@endcond }; /** Airport. * @see https://schema.org/Airport. */ class KITINERARY_EXPORT Airport : public Place { KITINERARY_GADGET(Airport) KITINERARY_PROPERTY(QString, iataCode, setIataCode) }; /** Train station. * @see https://schema.org/TrainStation */ class KITINERARY_EXPORT TrainStation : public Place { KITINERARY_GADGET(TrainStation) }; /** Bus station. * @see https://schema.org/BusStation */ class KITINERARY_EXPORT BusStation : public Place { KITINERARY_GADGET(BusStation) }; /** Tourist attraction (e.g. Museum, sight, etc.). * @see https://schema.org/TouristAttraction */ class KITINERARY_EXPORT TouristAttraction : public Place { KITINERARY_GADGET(TouristAttraction) }; } Q_DECLARE_METATYPE(KItinerary::Place) Q_DECLARE_METATYPE(KItinerary::GeoCoordinates) Q_DECLARE_METATYPE(KItinerary::PostalAddress) Q_DECLARE_METATYPE(KItinerary::Airport) Q_DECLARE_METATYPE(KItinerary::TrainStation) Q_DECLARE_METATYPE(KItinerary::BusStation) Q_DECLARE_METATYPE(KItinerary::TouristAttraction) #endif // KITINERARY_PLACE_H diff --git a/src/datatypes/rentalcar.cpp b/src/datatypes/rentalcar.cpp index 6ec1e73..414bd23 100644 --- a/src/datatypes/rentalcar.cpp +++ b/src/datatypes/rentalcar.cpp @@ -1,40 +1,44 @@ /* Copyright (C) 2018 Laurent Montel 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 "rentalcar.h" #include "datatypes_p.h" #include #include using namespace KItinerary; namespace KItinerary { class RentalCarPrivate: public QSharedData { public: QString name; QString model; + Organization rentalCompany; + Brand brand; }; KITINERARY_MAKE_SIMPLE_CLASS(RentalCar) KITINERARY_MAKE_PROPERTY(RentalCar, QString, name, setName) KITINERARY_MAKE_PROPERTY(RentalCar, QString, model, setModel) +KITINERARY_MAKE_PROPERTY(RentalCar, Organization, rentalCompany, setRentalCompany) +KITINERARY_MAKE_PROPERTY(RentalCar, Brand, brand, setBrand) } #include "moc_rentalcar.cpp" diff --git a/src/datatypes/rentalcar.h b/src/datatypes/rentalcar.h index c3d509a..1b72af4 100644 --- a/src/datatypes/rentalcar.h +++ b/src/datatypes/rentalcar.h @@ -1,45 +1,49 @@ /* Copyright (C) 2018 Laurent Montel 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_RENTALCAR_H #define KITINERARY_RENTALCAR_H #include "kitinerary_export.h" #include "datatypes.h" +#include "organization.h" +#include "brand.h" namespace KItinerary { class RentalCarPrivate; -/** An event. - * @see https://developers.google.com/gmail/markup/reference/event-reservation +/** A car rental. + * @see https://developers.google.com/gmail/markup/reference/rental-car */ class KITINERARY_EXPORT RentalCar { KITINERARY_GADGET(RentalCar) KITINERARY_PROPERTY(QString, name, setName) KITINERARY_PROPERTY(QString, model, setModel) - //Add more info : brand/rentalcompany + KITINERARY_PROPERTY(KItinerary::Organization, rentalCompany, setRentalCompany) + KITINERARY_PROPERTY(KItinerary::Brand, brand, setBrand) + private: QExplicitlySharedDataPointer d; }; } Q_DECLARE_METATYPE(KItinerary::RentalCar) #endif // KITINERARY_RENTALCAR_H diff --git a/src/extractorpostprocessor.cpp b/src/extractorpostprocessor.cpp index c9ff9f2..202b8f9 100644 --- a/src/extractorpostprocessor.cpp +++ b/src/extractorpostprocessor.cpp @@ -1,618 +1,611 @@ /* 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 "config-kitinerary.h" #include "extractorpostprocessor.h" #include "iatabcbpparser.h" #include "jsonlddocument.h" #include "logging.h" #include "mergeutil.h" #include "sortutil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KCONTACTS #include #endif #include #include #include #include #include #include using namespace KItinerary; namespace KItinerary { class ExtractorPostprocessorPrivate { public: void mergeOrAppend(const QVariant &elem); QVariant processFlightReservation(FlightReservation res) const; Flight processFlight(Flight flight) const; Airport processAirport(Airport airport) const; Airline processAirline(Airline airline) const; QDateTime processFlightTime(QDateTime dt, const Flight &flight, const Airport &airport) const; TrainReservation processTrainReservation(TrainReservation res) const; TrainTrip processTrainTrip(TrainTrip trip) const; TrainStation processTrainStation(TrainStation station) const; QDateTime processTrainTripTime(QDateTime dt, const TrainStation &station) const; BusReservation processBusReservation(BusReservation res) const; BusTrip processBusTrip(BusTrip trip) const; LodgingReservation processLodgingReservation(LodgingReservation res) const; FoodEstablishmentReservation processFoodEstablishmentReservation(FoodEstablishmentReservation res) const; TouristAttractionVisit processTouristAttractionVisit(TouristAttractionVisit visit) const; EventReservation processEventReservation(EventReservation res) const; RentalCarReservation processRentalCarReservation(RentalCarReservation res) const; TaxiReservation processTaxiReservation(TaxiReservation res) const; Event processEvent(Event event) const; template T processReservation(T res) const; Person processPerson(Person person) const; template T processPlace(T place) const; QVariantList processActions(QVariantList actions) const; bool filterReservation(const QVariant &res) const; bool filterLodgingReservation(const LodgingReservation &res) const; bool filterFlight(const Flight &flight) const; bool filterAirport(const Airport &airport) const; template bool filterTrainOrBusTrip(const T &trip) const; template bool filterTrainOrBusStation(const T &station) const; bool filterEventReservation(const EventReservation &res) const; bool filterFoodReservation(const FoodEstablishmentReservation &res) const; QVector m_data; QDateTime m_contextDate; bool m_resultFinalized = false; }; } ExtractorPostprocessor::ExtractorPostprocessor() : d(new ExtractorPostprocessorPrivate) { } ExtractorPostprocessor::ExtractorPostprocessor(ExtractorPostprocessor &&) noexcept = default; ExtractorPostprocessor::~ExtractorPostprocessor() = default; void ExtractorPostprocessor::process(const QVector &data) { - qDebug() << "======"; - qDebug() << JsonLdDocument::toJson(data); - qDebug() << "-----"; - qDebug() << data; - qDebug() << "~~~~"; - d->m_resultFinalized = false; d->m_data.reserve(d->m_data.size() + data.size()); for (auto elem : data) { if (JsonLd::isA(elem)) { elem = d->processFlightReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTrainReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processLodgingReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processFoodEstablishmentReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTouristAttractionVisit(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processBusReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processEventReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processRentalCarReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTaxiReservation(elem.value()); } d->mergeOrAppend(elem); } } QVector ExtractorPostprocessor::result() const { if (!d->m_resultFinalized) { for (auto it = d->m_data.begin(); it != d->m_data.end();) { if (d->filterReservation(*it)) { ++it; } else { //qCDebug(Log).noquote() << "Discarding element:" << QJsonDocument(JsonLdDocument::toJson({*it})).toJson(); it = d->m_data.erase(it); } } d->m_resultFinalized = true; } std::stable_sort(d->m_data.begin(), d->m_data.end(), SortUtil::isBefore); return d->m_data; } void ExtractorPostprocessor::setContextDate(const QDateTime& dt) { d->m_contextDate = dt; } void ExtractorPostprocessorPrivate::mergeOrAppend(const QVariant &elem) { const auto it = std::find_if(m_data.begin(), m_data.end(), [elem](const QVariant &other) { return MergeUtil::isSame(elem, other); }); if (it == m_data.end()) { m_data.push_back(elem); } else { *it = JsonLdDocument::apply(*it, elem); } } QVariant ExtractorPostprocessorPrivate::processFlightReservation(FlightReservation res) const { // expand ticketToken for IATA BCBP data const auto bcbp = res.reservedTicket().value().ticketTokenData(); if (!bcbp.isEmpty()) { const auto bcbpData = IataBcbpParser::parse(bcbp, m_contextDate.date()); if (bcbpData.size() == 1) { res = JsonLdDocument::apply(bcbpData.at(0), res).value(); } else { for (const auto &data : bcbpData) { if (MergeUtil::isSame(res, data)) { res = JsonLdDocument::apply(data, res).value(); break; } } } } res.setReservationFor(processFlight(res.reservationFor().value())); return processReservation(res); } Flight ExtractorPostprocessorPrivate::processFlight(Flight flight) const { flight.setDepartureAirport(processAirport(flight.departureAirport())); flight.setArrivalAirport(processAirport(flight.arrivalAirport())); flight.setAirline(processAirline(flight.airline())); flight.setBoardingTime(processFlightTime(flight.boardingTime(), flight, flight.departureAirport())); flight.setDepartureTime(processFlightTime(flight.departureTime(), flight, flight.departureAirport())); flight.setArrivalTime(processFlightTime(flight.arrivalTime(), flight, flight.arrivalAirport())); return flight; } Airport ExtractorPostprocessorPrivate::processAirport(Airport airport) const { // clean up name airport.setName(airport.name().trimmed()); // complete missing IATA codes auto iataCode = airport.iataCode(); if (iataCode.isEmpty()) { iataCode = KnowledgeDb::iataCodeFromName(airport.name()).toString(); if (!iataCode.isEmpty()) { airport.setIataCode(iataCode); } } // complete missing geo coordinates auto geo = airport.geo(); if (!geo.isValid()) { const auto coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{iataCode}); if (coord.isValid()) { geo.setLatitude(coord.latitude); geo.setLongitude(coord.longitude); airport.setGeo(geo); } } // add country auto addr = airport.address(); if (addr.addressCountry().isEmpty()) { const auto isoCode = KnowledgeDb::countryForAirport(KnowledgeDb::IataCode{iataCode}); if (isoCode.isValid()) { addr.setAddressCountry(isoCode.toString()); airport.setAddress(addr); } } return processPlace(airport); } Airline ExtractorPostprocessorPrivate::processAirline(Airline airline) const { airline.setName(airline.name().trimmed()); return airline; } QDateTime ExtractorPostprocessorPrivate::processFlightTime(QDateTime dt, const Flight &flight, const Airport &airport) const { if (!dt.isValid()) { return dt; } if (dt.date().year() <= 1970 && flight.departureDay().isValid()) { // we just have the time, but not the day dt.setDate(flight.departureDay()); } if (dt.timeSpec() == Qt::TimeZone || airport.iataCode().isEmpty()) { return dt; } const auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{airport.iataCode()}); if (!tz.isValid()) { return dt; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return dt; } if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) { dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); } else if (dt.timeSpec() == Qt::UTC) { dt = dt.toTimeZone(tz); } return dt; } TrainReservation ExtractorPostprocessorPrivate::processTrainReservation(TrainReservation res) const { res.setReservationFor(processTrainTrip(res.reservationFor().value())); return processReservation(res); } TrainTrip ExtractorPostprocessorPrivate::processTrainTrip(TrainTrip trip) const { trip.setArrivalPlatform(trip.arrivalPlatform().trimmed()); trip.setDeparturePlatform(trip.departurePlatform().trimmed()); trip.setDeparatureStation(processTrainStation(trip.departureStation())); trip.setArrivalStation(processTrainStation(trip.arrivalStation())); trip.setDepartureTime(processTrainTripTime(trip.departureTime(), trip.departureStation())); trip.setArrivalTime(processTrainTripTime(trip.arrivalTime(), trip.arrivalStation())); return trip; } TrainStation ExtractorPostprocessorPrivate::processTrainStation(TrainStation station) const { const auto id = station.identifier(); if (id.isEmpty()) { // empty -> null cleanup, to have more compact json-ld output station.setIdentifier(QString()); } else if (id.startsWith(QLatin1String("sncf:")) && id.size() == 10) { // Gare & Connexion ids start with a country code, propagate that to the station address field auto addr = station.address(); if (addr.addressCountry().isEmpty()) { addr.setAddressCountry(id.mid(5, 2).toUpper()); station.setAddress(addr); } const auto record = KnowledgeDb::stationForGaresConnexionsId(KnowledgeDb::GaresConnexionsId{id.mid(5)}); if (!station.geo().isValid() && record.coordinate.isValid()) { GeoCoordinates geo; geo.setLatitude(record.coordinate.latitude); geo.setLongitude(record.coordinate.longitude); station.setGeo(geo); } if (addr.addressCountry().isEmpty() && record.country.isValid()) { addr.setAddressCountry(record.country.toString()); station.setAddress(addr); } } else if (id.startsWith(QLatin1String("ibnr:")) && id.size() == 12) { const auto record = KnowledgeDb::stationForIbnr(KnowledgeDb::IBNR{id.mid(5).toUInt()}); if (!station.geo().isValid() && record.coordinate.isValid()) { GeoCoordinates geo; geo.setLatitude(record.coordinate.latitude); geo.setLongitude(record.coordinate.longitude); station.setGeo(geo); } auto addr = station.address(); if (addr.addressCountry().isEmpty() && record.country.isValid()) { addr.setAddressCountry(record.country.toString()); station.setAddress(addr); } } return processPlace(station); } QDateTime ExtractorPostprocessorPrivate::processTrainTripTime(QDateTime dt, const TrainStation& station) const { if (!dt.isValid()) { return dt; } if (dt.timeSpec() == Qt::TimeZone || station.identifier().isEmpty()) { return dt; } QTimeZone tz; if (station.identifier().startsWith(QLatin1String("sncf:"))) { const auto record = KnowledgeDb::stationForGaresConnexionsId(KnowledgeDb::GaresConnexionsId{station.identifier().mid(5)}); tz = record.timezone.toQTimeZone(); } else if (station.identifier().startsWith(QLatin1String("ibnr:"))) { const auto record = KnowledgeDb::stationForIbnr(KnowledgeDb::IBNR{station.identifier().mid(5).toUInt()}); tz = record.timezone.toQTimeZone(); } if (!tz.isValid()) { return dt; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return dt; } if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) { dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); } else if (dt.timeSpec() == Qt::UTC) { dt = dt.toTimeZone(tz); } return dt; } BusReservation ExtractorPostprocessorPrivate::processBusReservation(BusReservation res) const { res.setReservationFor(processBusTrip(res.reservationFor().value())); return processReservation(res); } BusTrip ExtractorPostprocessorPrivate::processBusTrip(BusTrip trip) const { trip.setDepartureStation(processPlace(trip.departureStation())); trip.setArrivalStation(processPlace(trip.arrivalStation())); return trip; } LodgingReservation ExtractorPostprocessorPrivate::processLodgingReservation(LodgingReservation res) const { res.setReservationFor(processPlace(res.reservationFor().value())); return processReservation(res); } TaxiReservation ExtractorPostprocessorPrivate::processTaxiReservation(TaxiReservation res) const { res.setPickupLocation(processPlace(res.pickupLocation())); return processReservation(res); } RentalCarReservation ExtractorPostprocessorPrivate::processRentalCarReservation(RentalCarReservation res) const { - qDebug() << JsonLdDocument::toJson({res}); res.setPickupLocation(processPlace(res.pickupLocation())); res.setDropoffLocation(processPlace(res.dropoffLocation())); return processReservation(res); } FoodEstablishmentReservation ExtractorPostprocessorPrivate::processFoodEstablishmentReservation(FoodEstablishmentReservation res) const { res.setReservationFor(processPlace(res.reservationFor().value())); return processReservation(res); } TouristAttractionVisit ExtractorPostprocessorPrivate::processTouristAttractionVisit(TouristAttractionVisit visit) const { visit.setTouristAttraction(processPlace(visit.touristAttraction())); return visit; } EventReservation ExtractorPostprocessorPrivate::processEventReservation(EventReservation res) const { res.setReservationFor(processEvent(res.reservationFor().value())); return processReservation(res); } Event ExtractorPostprocessorPrivate::processEvent(Event event) const { // normalize location to be a Place if (JsonLd::isA(event.location())) { Place place; place.setAddress(event.location().value()); event.setLocation(place); } if (JsonLd::isA(event.location())) { event.setLocation(processPlace(event.location().value())); } return event; } template T ExtractorPostprocessorPrivate::processReservation(T res) const { res.setUnderName(processPerson(res.underName().template value())); res.setPotentialAction(processActions(res.potentialAction())); return res; } Person ExtractorPostprocessorPrivate::processPerson(Person person) const { person.setName(person.name().simplified()); if (person.name().isEmpty() && !person.familyName().isEmpty() && !person.givenName().isEmpty()) { person.setName(person.givenName() + QLatin1Char(' ') + person.familyName()); } // strip prefixes, they break comparisons static const char* honorificPrefixes[] = { "MR ", "MS ", "MRS " }; for (auto prefix : honorificPrefixes) { if (person.name().startsWith(QLatin1String(prefix), Qt::CaseInsensitive)) { person.setName(person.name().mid(strlen(prefix))); break; } } return person; } template T ExtractorPostprocessorPrivate::processPlace(T place) const { #ifdef HAVE_KCONTACTS auto addr = place.address(); if (!addr.addressCountry().isEmpty() && addr.addressCountry().size() != 2) { const auto isoCode = KContacts::Address::countryToISO(addr.addressCountry()).toUpper(); if (!isoCode.isEmpty()) { addr.setAddressCountry(isoCode); place.setAddress(addr); } } #endif return place; } QVariantList ExtractorPostprocessorPrivate::processActions(QVariantList actions) const { // remove non-actions and actions with invalid URLs QUrl viewUrl; for (auto it = actions.begin(); it != actions.end();) { if (!JsonLd::canConvert(*it)) { it = actions.erase(it); continue; } const auto action = JsonLd::convert(*it); if (!action.target().isValid()) { it = actions.erase(it); continue; } if (JsonLd::isA(*it)) { viewUrl = action.target(); } ++it; } // normalize the order, so JSON comparisson still yields correct results std::sort(actions.begin(), actions.end(), [](const QVariant &lhs, const QVariant &rhs) { return strcmp(lhs.typeName(), rhs.typeName()) < 0; }); // remove actions that don't actually have their own target, or duplicates QUrl prevUrl; const char* prevType = nullptr; for (auto it = actions.begin(); it != actions.end();) { const auto action = JsonLd::convert(*it); const auto isDuplicate = action.target() == prevUrl && (prevType ? strcmp(prevType, (*it).typeName()) == 0 : false); if ((JsonLd::isA(*it) || action.target() != viewUrl) && !isDuplicate) { prevUrl = action.target(); prevType = (*it).typeName(); ++it; } else { it = actions.erase(it); } } return actions; } bool ExtractorPostprocessorPrivate::filterReservation(const QVariant &res) const { if (JsonLd::isA(res)) { return filterFlight(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterTrainOrBusTrip(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterTrainOrBusTrip(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterLodgingReservation(res.value()); } if (JsonLd::isA(res)) { return filterEventReservation(res.value()); } if (JsonLd::isA(res)) { return filterFoodReservation(res.value()); } // types without specific filters yet if (JsonLd::isA(res) || JsonLd::isA(res) || JsonLd::isA(res)) { return true; } // unknown top-level type return false; } bool ExtractorPostprocessorPrivate::filterLodgingReservation(const LodgingReservation &res) const { return res.checkinTime().isValid() && res.checkoutTime().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(); } template bool ExtractorPostprocessorPrivate::filterTrainOrBusTrip(const T &trip) const { return filterTrainOrBusStation(trip.departureStation()) && filterTrainOrBusStation(trip.arrivalStation()) && trip.departureTime().isValid() && trip.arrivalTime().isValid(); } template bool ExtractorPostprocessorPrivate::filterTrainOrBusStation(const T &station) const { return !station.name().isEmpty(); } bool ExtractorPostprocessorPrivate::filterEventReservation(const EventReservation &res) const { const auto event = res.reservationFor().value(); return !event.name().isEmpty() && event.startDate().isValid(); } bool ExtractorPostprocessorPrivate::filterFoodReservation(const FoodEstablishmentReservation &res) const { return res.startTime().isValid(); } diff --git a/src/jsonlddocument.cpp b/src/jsonlddocument.cpp index ef09631..3f38c69 100644 --- a/src/jsonlddocument.cpp +++ b/src/jsonlddocument.cpp @@ -1,387 +1,389 @@ /* 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 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 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])); } 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) { const auto var = createInstance(elem.toObject()); if (!var.isNull()) { l.push_back(var); } } } 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(DownloadAction); 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(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 = 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(); } if (v.canConvert()) { QSequentialIterable iterable = v.value(); if (iterable.size() == 0) { return {}; } QJsonArray array; for (const auto &var : iterable) { array.push_back(toJson(var)); } return array; } 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; } 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; } 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; }