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