From 00a86d9d2c12aa4e774b1d2c9aec3743db985876 Mon Sep 17 00:00:00 2001 From: Alexey Edelev Date: Fri, 10 Jan 2025 18:04:59 +0100 Subject: [PATCH] Handle special cases of JSON serializing of the Well-known types Change the way we handle them. Add the special registry so any type can register its special JSON (de)serializer independently. Move the timestamp serializer from the generic JSON serializer implementation to QtProtobufWellknownTypes library so we remove the weak backward link from QtProtobuf module to its dependency. The introduced mechanism is also scalable, and allows adding other types that have similar special JSON serialization. Task-number: QTBUG-130555 Task-number: QTBUG-120214 Change-Id: I56ce2e43a00262069281871d5f903f1e94abef83 Reviewed-by: Dennis Oberst --- src/protobuf/CMakeLists.txt | 2 +- src/protobuf/qprotobufjsonserializer.cpp | 150 +++++++++--------- src/protobuf/qprotobufjsonserializer_p.h | 45 ++++++ .../messagedefinitionprinter.cpp | 5 + .../qtprotobufgen/qprotobufgenerator.cpp | 5 +- src/wellknown/CMakeLists.txt | 1 + ...qprotobufwellknowntypesjsonserializers.cpp | 83 ++++++++++ ...qprotobufwellknowntypesjsonserializers_p.h | 28 ++++ 8 files changed, 242 insertions(+), 77 deletions(-) create mode 100644 src/protobuf/qprotobufjsonserializer_p.h create mode 100644 src/wellknown/qprotobufwellknowntypesjsonserializers.cpp create mode 100644 src/wellknown/qprotobufwellknowntypesjsonserializers_p.h diff --git a/src/protobuf/CMakeLists.txt b/src/protobuf/CMakeLists.txt index 29715fa6..bd965bf8 100644 --- a/src/protobuf/CMakeLists.txt +++ b/src/protobuf/CMakeLists.txt @@ -9,7 +9,7 @@ qt_internal_add_module(Protobuf qtprotobufglobal.h qabstractprotobufserializer.cpp qabstractprotobufserializer.h qprotobufdeserializerbase_p.h qprotobufdeserializerbase.cpp - qprotobufjsonserializer.cpp qprotobufjsonserializer.h + qprotobufjsonserializer.cpp qprotobufjsonserializer.h qprotobufjsonserializer_p.h qprotobuflazymessagepointer.h qprotobufmessage.cpp qprotobufmessage.h qprotobufmessage_p.h qprotobufobject.h diff --git a/src/protobuf/qprotobufjsonserializer.cpp b/src/protobuf/qprotobufjsonserializer.cpp index b237a88b..2933a8eb 100644 --- a/src/protobuf/qprotobufjsonserializer.cpp +++ b/src/protobuf/qprotobufjsonserializer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -45,6 +47,41 @@ using namespace ProtobufScalarJsonSerializers; namespace { +struct JsonHandlerRegistry +{ + void registerHandler(QMetaType metaType, QtProtobufPrivate::CustomJsonSerializer serializer, + QtProtobufPrivate::CustomJsonDeserializer deserializer) + { + QWriteLocker locker(&m_lock); + m_registry[metaType] = { serializer, deserializer }; + } + + QtProtobufPrivate::CustomJsonSerializer findSerializer(QMetaType metaType) + { + QReadLocker locker(&m_lock); + const auto it = m_registry.constFind(metaType); + if (it != m_registry.constEnd()) + return it.value().first; + return nullptr; + } + + QtProtobufPrivate::CustomJsonDeserializer findDeserializer(QMetaType metaType) + { + QReadLocker locker(&m_lock); + const auto it = m_registry.constFind(metaType); + if (it != m_registry.constEnd()) + return it.value().second; + return nullptr; + } + +private: + using Handler = std::pair; + QReadWriteLock m_lock; + QHash m_registry; +}; +Q_GLOBAL_STATIC(JsonHandlerRegistry, jsonHandlersRegistry) + inline QString convertJsonKeyToJsonName(QStringView name) { QString result; @@ -63,6 +100,27 @@ inline QString convertJsonKeyToJsonName(QStringView name) } +void QtProtobufPrivate::registerCustomJsonHandler(QMetaType metaType, + QtProtobufPrivate::CustomJsonSerializer + serializer, + QtProtobufPrivate::CustomJsonDeserializer + deserializer) +{ + jsonHandlersRegistry->registerHandler(metaType, serializer, deserializer); +} + +QtProtobufPrivate::CustomJsonSerializer +QtProtobufPrivate::findCustomJsonSerializer(QMetaType metaType) +{ + return jsonHandlersRegistry->findSerializer(metaType); +} + +QtProtobufPrivate::CustomJsonDeserializer +QtProtobufPrivate::findCustomJsonDeserializer(QMetaType metaType) +{ + return jsonHandlersRegistry->findDeserializer(metaType); +} + class QProtobufJsonSerializerImpl final : public QProtobufSerializerBase { public: @@ -84,9 +142,6 @@ private: void serializeMessageFieldEnd(const QProtobufMessage *message, const QProtobufFieldInfo &fieldInfo) override; - void serializeTimestamp(const QProtobufMessage *message, - const QtProtobufPrivate::QProtobufFieldInfo &fieldInfo); - QJsonObject m_result; QList m_state; @@ -115,8 +170,6 @@ private: int nextFieldIndex(QProtobufMessage *message) override; bool deserializeScalarField(QVariant &, const QtProtobufPrivate::QProtobufFieldInfo &) override; - [[nodiscard]] bool deserializeTimestamp(QProtobufMessage *message); - struct JsonDeserializerState { JsonDeserializerState(const QJsonObject &obj) : obj(obj) { } @@ -194,43 +247,19 @@ void QProtobufJsonSerializerImpl::serializeMessageField(const QProtobufMessage * const QtProtobufPrivate::QProtobufFieldInfo &fieldInfo) { - if (message->propertyOrdering() - ->messageFullName() - .compare(QString::fromUtf8("google.protobuf.Timestamp")) - == 0) { - serializeTimestamp(message, fieldInfo); + if (!message) + return; + + const auto *metaObject = QtProtobufSerializerHelpers::messageMetaObject(message); + + if (auto *serializer = QtProtobufPrivate::findCustomJsonSerializer(metaObject->metaType())) { + if (const QJsonValue value = serializer(message); !value.isUndefined()) + m_result.insert(fieldInfo.jsonName().toString(), value); } else { QProtobufSerializerBase::serializeMessageField(message, fieldInfo); } } -void QProtobufJsonSerializerImpl::serializeTimestamp(const QProtobufMessage *message, - const QtProtobufPrivate::QProtobufFieldInfo - &fieldInfo) -{ - qint64 secs = 0; - qint32 nanos = 0; - - if (const auto secondsValue = message->property("seconds"); secondsValue.canConvert()) { - secs = secondsValue.value(); - } else { - qWarning() << "QProtobufJsonSerializerImpl::serializeTimestamp() failed to convert seconds"; - } - - if (const auto nanosValue = message->property("nanos"); nanosValue.canConvert()) { - nanos = nanosValue.value(); - } else { - qWarning() << "QProtobufJsonSerializerImpl::serializeTimestamp() failed to convert nanos"; - } - - const auto datetime = QDateTime::fromMSecsSinceEpoch(secs * 1000 + nanos / 1000000, - QTimeZone::UTC); - const auto datetimeISO = datetime.toString(Qt::ISODateWithMs); - - const auto jsonName = fieldInfo.jsonName(); - m_result.insert(jsonName.toString(), serializeCommon(datetimeISO)); -} - bool QProtobufJsonSerializerImpl::serializeEnum(QVariant &value, const QProtobufFieldInfo &fieldInfo) @@ -328,13 +357,15 @@ void QProtobufJsonDeserializerImpl::setError(QAbstractProtobufSerializer::Error bool QProtobufJsonDeserializerImpl::deserializeMessageField(QProtobufMessage *message) { - if (!m_state.last().scalarValue.isNull()) { - if (message->propertyOrdering() - ->messageFullName() - .compare(QString::fromUtf8("google.protobuf.Timestamp")) - == 0 - && m_state.last().scalarValue.isString()) { - return deserializeTimestamp(message); + if (!message) + return true; + + const auto &value = m_state.last().scalarValue; + if (!value.isNull()) { + const auto *metaObject = QtProtobufSerializerHelpers::messageMetaObject(message); + if (auto *deserializer = QtProtobufPrivate::findCustomJsonDeserializer(metaObject + ->metaType())) { + return deserializer(message, value); } setInvalidFormatError(); return false; @@ -342,37 +373,6 @@ bool QProtobufJsonDeserializerImpl::deserializeMessageField(QProtobufMessage *me return QProtobufDeserializerBase::deserializeMessageField(message); } -bool QProtobufJsonDeserializerImpl::deserializeTimestamp(QProtobufMessage *message) -{ - const auto tsString = m_state.last().scalarValue.toString(); - // Protobuf requires upper-case letters in timestamp string be case sensitive. - if (tsString.toUpper() != tsString) - return false; - - if (tsString.contains(u' ')) - return false; - - //Ensure the field either ends with Z or a valid offset - static const QRegularExpression TimeStampEnding(".+([\\+\\-]\\d{2}:\\d{2}|Z)$"_L1); - if (!TimeStampEnding.match(tsString).hasMatch()) - return false; - - const auto datetime = QDateTime::fromString(tsString, Qt::ISODateWithMs); - - if (!datetime.isValid()) { - qWarning() << "QProtobufJsonDeserializerImpl::deserializeTimestamp() datetime is invalid"; - return false; - } - const auto msecs = datetime.toMSecsSinceEpoch(); - const qint64 seconds = msecs / 1000; - const qint32 nanos = (msecs % 1000) * 1000000; - - message->setProperty("seconds", QVariant::fromValue(seconds)); - message->setProperty("nanos", QVariant::fromValue(nanos)); - - return true; -} - bool QProtobufJsonDeserializerImpl::deserializeEnum(QVariant &value, const QtProtobufPrivate::QProtobufFieldInfo &fieldInfo) diff --git a/src/protobuf/qprotobufjsonserializer_p.h b/src/protobuf/qprotobufjsonserializer_p.h new file mode 100644 index 00000000..af98578b --- /dev/null +++ b/src/protobuf/qprotobufjsonserializer_p.h @@ -0,0 +1,45 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QPROTOBUFJSONSERIALIZER_P_H +#define QPROTOBUFJSONSERIALIZER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +#include + +QT_BEGIN_NAMESPACE + +class QProtobufMessage; +class QJsonValue; +class QMetaType; + +namespace QtProtobufPrivate { + +struct QProtobufFieldInfo; + +using CustomJsonSerializer = QJsonValue (*)(const QProtobufMessage *); +using CustomJsonDeserializer = bool (*)(QProtobufMessage *, const QJsonValue &); + +Q_PROTOBUF_EXPORT void registerCustomJsonHandler(QMetaType metaType, + CustomJsonSerializer serializer, + CustomJsonDeserializer deserializer); + +[[nodiscard]] CustomJsonSerializer findCustomJsonSerializer(QMetaType metaType); +[[nodiscard]] CustomJsonDeserializer findCustomJsonDeserializer(QMetaType metaType); +} + +QT_END_NAMESPACE + +#endif // QPROTOBUFJSONSERIALIZER_P_H diff --git a/src/tools/qtprotobufgen/messagedefinitionprinter.cpp b/src/tools/qtprotobufgen/messagedefinitionprinter.cpp index 411e0c90..5fbaf952 100644 --- a/src/tools/qtprotobufgen/messagedefinitionprinter.cpp +++ b/src/tools/qtprotobufgen/messagedefinitionprinter.cpp @@ -149,6 +149,11 @@ void MessageDefinitionPrinter::printRegisterBody() if (m_descriptor->full_name() == "google.protobuf.Any") m_printer->Print("QT_PREPEND_NAMESPACE(QtProtobuf)::Any::registerTypes();\n"); + if (m_descriptor->full_name() == "google.protobuf.Timestamp") { + m_printer->Print("QT_PREPEND_NAMESPACE(QtProtobufWellKnownTypesPrivate)::" + "registerTimestampCustomJsonHandler();\n"); + } + common::iterateMessageFields( m_descriptor, [&](const FieldDescriptor *field, const PropertyMap &propertyMap) { auto it = propertyMap.find("full_type"); diff --git a/src/tools/qtprotobufgen/qprotobufgenerator.cpp b/src/tools/qtprotobufgen/qprotobufgenerator.cpp index c5f7341d..f551444b 100644 --- a/src/tools/qtprotobufgen/qprotobufgenerator.cpp +++ b/src/tools/qtprotobufgen/qprotobufgenerator.cpp @@ -73,8 +73,11 @@ void QProtobufGenerator::GenerateSources(const FileDescriptor *file, return; } }); - if (generateWellknownTimestamp) + if (generateWellknownTimestamp) { + externalIncludes + .insert("QtProtobufWellKnownTypes/private/qprotobufwellknowntypesjsonserializers_p.h"); externalIncludes.insert("QtCore/QTimeZone"); + } printIncludes(sourcePrinter.get(), internalIncludes, externalIncludes, { "cmath" }); OpenFileNamespaces(file, sourcePrinter.get()); diff --git a/src/wellknown/CMakeLists.txt b/src/wellknown/CMakeLists.txt index 8ea307a9..3c7f55d9 100644 --- a/src/wellknown/CMakeLists.txt +++ b/src/wellknown/CMakeLists.txt @@ -5,6 +5,7 @@ qt_internal_add_protobuf_module(ProtobufWellKnownTypes SOURCES qtprotobufwellknowntypesglobal.h qprotobufanysupport.cpp qprotobufanysupport.h + qprotobufwellknowntypesjsonserializers_p.h qprotobufwellknowntypesjsonserializers.cpp PUBLIC_LIBRARIES Qt::Protobuf LIBRARIES diff --git a/src/wellknown/qprotobufwellknowntypesjsonserializers.cpp b/src/wellknown/qprotobufwellknowntypesjsonserializers.cpp new file mode 100644 index 00000000..38fa38ed --- /dev/null +++ b/src/wellknown/qprotobufwellknowntypesjsonserializers.cpp @@ -0,0 +1,83 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include +#include + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +namespace { +QJsonValue serializeProtobufWellKnownTimestamp(const QProtobufMessage *message) +{ + qint64 secs = 0; + qint32 nanos = 0; + + if (const auto secondsValue = message->property("seconds"); secondsValue.canConvert()) { + secs = secondsValue.value(); + } else { + qWarning() << "serializeTimestamp() failed to convert seconds"; + } + + if (const auto nanosValue = message->property("nanos"); nanosValue.canConvert()) { + nanos = nanosValue.value(); + } else { + qWarning() << "serializeTimestamp() failed to convert nanos"; + } + + const auto datetime = QDateTime::fromMSecsSinceEpoch(secs * 1000 + nanos / 1000000, + QTimeZone::UTC); + return ProtobufScalarJsonSerializers::serializeCommon< + QString>(datetime.toString(Qt::ISODateWithMs)); +} + +bool deserializeProtobufWellKnownTimestamp(QProtobufMessage *message, const QJsonValue &value) +{ + if (!value.isString()) + return false; + + const auto tsString = value.toString(); + // Protobuf requires upper-case letters in timestamp string be case sensitive. + if (tsString.toUpper() != tsString) + return false; + + if (tsString.contains(u' ')) + return false; + + //Ensure the field either ends with Z or a valid offset + static const QRegularExpression TimeStampEnding(".+([\\+\\-]\\d{2}:\\d{2}|Z)$"_L1); + if (!TimeStampEnding.match(tsString).hasMatch()) + return false; + + const auto datetime = QDateTime::fromString(tsString, Qt::ISODateWithMs); + + if (!datetime.isValid()) { + qWarning() << "deserializeTimestamp() datetime is invalid"; + return false; + } + const auto msecs = datetime.toMSecsSinceEpoch(); + const qint64 seconds = msecs / 1000; + const qint32 nanos = (msecs % 1000) * 1000000; + + message->setProperty("seconds", QVariant::fromValue(seconds)); + message->setProperty("nanos", QVariant::fromValue(nanos)); + + return true; +} + +} // namespace + +void QtProtobufWellKnownTypesPrivate::registerTimestampCustomJsonHandler() +{ + QtProtobufPrivate::registerCustomJsonHandler(QMetaType::fromType(), + serializeProtobufWellKnownTimestamp, + deserializeProtobufWellKnownTimestamp); +} + +QT_END_NAMESPACE diff --git a/src/wellknown/qprotobufwellknowntypesjsonserializers_p.h b/src/wellknown/qprotobufwellknowntypesjsonserializers_p.h new file mode 100644 index 00000000..86ec4de2 --- /dev/null +++ b/src/wellknown/qprotobufwellknowntypesjsonserializers_p.h @@ -0,0 +1,28 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H +#define QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +QT_BEGIN_NAMESPACE + +namespace QtProtobufWellKnownTypesPrivate { +void registerTimestampCustomJsonHandler(); +} + +QT_END_NAMESPACE + +#endif // QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H