From 91a5382d81b0c596e07ba03b886d28101af5ce34 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Thu, 23 May 2024 21:48:37 +0200 Subject: [PATCH] [OAPIF provider] addFeatures(): issue a /items/{id} request to get a refreshed version of the feature Fixes #57486 --- src/providers/wfs/CMakeLists.txt | 1 + src/providers/wfs/oapif/qgsoapifprovider.cpp | 26 +++++ .../wfs/oapif/qgsoapifsingleitemrequest.cpp | 106 ++++++++++++++++++ .../wfs/oapif/qgsoapifsingleitemrequest.h | 74 ++++++++++++ tests/src/python/test_provider_oapif.py | 18 ++- 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/providers/wfs/oapif/qgsoapifsingleitemrequest.cpp create mode 100644 src/providers/wfs/oapif/qgsoapifsingleitemrequest.h diff --git a/src/providers/wfs/CMakeLists.txt b/src/providers/wfs/CMakeLists.txt index d08fcabb4f72..37b8cb1a2060 100644 --- a/src/providers/wfs/CMakeLists.txt +++ b/src/providers/wfs/CMakeLists.txt @@ -36,6 +36,7 @@ set(WFS_SRCS oapif/qgsoapifoptionsrequest.cpp oapif/qgsoapifprovider.cpp oapif/qgsoapifqueryablesrequest.cpp + oapif/qgsoapifsingleitemrequest.cpp oapif/qgsoapifutils.cpp ) diff --git a/src/providers/wfs/oapif/qgsoapifprovider.cpp b/src/providers/wfs/oapif/qgsoapifprovider.cpp index a7ec5bd082bf..884fdd55d300 100644 --- a/src/providers/wfs/oapif/qgsoapifprovider.cpp +++ b/src/providers/wfs/oapif/qgsoapifprovider.cpp @@ -29,6 +29,7 @@ #include "qgsoapifitemsrequest.h" #include "qgsoapifoptionsrequest.h" #include "qgsoapifqueryablesrequest.h" +#include "qgsoapifsingleitemrequest.h" #include "qgswfsconstants.h" #include "qgswfsutils.h" // for isCompatibleType() @@ -589,6 +590,31 @@ bool QgsOapifProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { f.setAttribute( idFieldIdx, id ); } + + // Refresh the feature content with its content from the server with a + // /items/{id} request. + if ( !( flags & QgsFeatureSink::FastInsert ) ) + { + QgsOapifSingleItemRequest itemRequest( mShared->mURI.uri(), mShared->appendExtraQueryParameters( mShared->mItemsUrl + QString( QStringLiteral( "/" ) + id ) ) ); + if ( itemRequest.request( /*synchronous=*/ true, /*forceRefresh=*/ true ) && + itemRequest.errorCode() == QgsBaseNetworkRequest::NoError ) + { + const QgsFeature &updatedFeature = itemRequest.feature(); + if ( updatedFeature.isValid() ) + { + int updatedFieldIdx = 0; + for ( const QgsField &updatedField : itemRequest.fields() ) + { + const int srcFieldIdx = mShared->mFields.indexOf( updatedField.name() ); + if ( srcFieldIdx >= 0 ) + { + f.setAttribute( srcFieldIdx, updatedFeature.attribute( updatedFieldIdx ) ); + } + updatedFieldIdx++; + } + } + } + } } QStringList::const_iterator idIt = jsonIds.constBegin(); diff --git a/src/providers/wfs/oapif/qgsoapifsingleitemrequest.cpp b/src/providers/wfs/oapif/qgsoapifsingleitemrequest.cpp new file mode 100644 index 000000000000..aa6aaeccbdcf --- /dev/null +++ b/src/providers/wfs/oapif/qgsoapifsingleitemrequest.cpp @@ -0,0 +1,106 @@ +/*************************************************************************** + qgsoapifsingleitemrequest.cpp + ----------------------------- + begin : May 2024 + copyright : (C) 2024 by Even Rouault + email : even.rouault at spatialys.com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgslogger.h" +#include "qgsoapifsingleitemrequest.h" +#include "qgsoapifutils.h" +#include "qgsproviderregistry.h" +#include "qgsvectordataprovider.h" + +#include "cpl_vsi.h" + +#include + +QgsOapifSingleItemRequest::QgsOapifSingleItemRequest( const QgsDataSourceUri &baseUri, const QString &url ): + QgsBaseNetworkRequest( QgsAuthorizationSettings( baseUri.username(), baseUri.password(), baseUri.authConfigId() ), tr( "OAPIF" ) ), + mUrl( url ) +{ + // Using Qt::DirectConnection since the download might be running on a different thread. + // In this case, the request was sent from the main thread and is executed with the main + // thread being blocked in future.waitForFinished() so we can run code on this object which + // lives in the main thread without risking havoc. + connect( this, &QgsBaseNetworkRequest::downloadFinished, this, &QgsOapifSingleItemRequest::processReply, Qt::DirectConnection ); +} + +bool QgsOapifSingleItemRequest::request( bool synchronous, bool forceRefresh ) +{ + QgsDebugMsgLevel( QStringLiteral( " QgsOapifSingleItemRequest::request() start time: %1" ).arg( time( nullptr ) ), 5 ); + if ( !sendGET( QUrl::fromEncoded( mUrl.toLatin1() ), QString( "application/geo+json, application/json" ), synchronous, forceRefresh ) ) + { + emit gotResponse(); + return false; + } + return true; +} + +QString QgsOapifSingleItemRequest::errorMessageWithReason( const QString &reason ) +{ + return tr( "Download of item failed: %1" ).arg( reason ); +} + +void QgsOapifSingleItemRequest::processReply() +{ + QgsDebugMsgLevel( QStringLiteral( "processReply start time: %1" ).arg( time( nullptr ) ), 5 ); + if ( mErrorCode != QgsBaseNetworkRequest::NoError ) + { + emit gotResponse(); + return; + } + QByteArray &buffer = mResponse; + if ( buffer.isEmpty() ) + { + mErrorMessage = tr( "empty response" ); + mErrorCode = QgsBaseNetworkRequest::ServerExceptionError; + emit gotResponse(); + return; + } + + if ( buffer.size() <= 200 ) + { + QgsDebugMsgLevel( QStringLiteral( "parsing item response: " ) + buffer, 4 ); + } + else + { + QgsDebugMsgLevel( QStringLiteral( "parsing item response: " ) + buffer.left( 100 ) + QStringLiteral( "[... snip ...]" ) + buffer.right( 100 ), 4 ); + } + + const QString vsimemFilename = QStringLiteral( "/vsimem/oaipf_%1.json" ).arg( reinterpret_cast< quintptr >( &buffer ), QT_POINTER_SIZE * 2, 16, QLatin1Char( '0' ) ); + VSIFCloseL( VSIFileFromMemBuffer( vsimemFilename.toUtf8().constData(), + const_cast( reinterpret_cast( buffer.constData() ) ), + buffer.size(), + false ) ); + QgsProviderRegistry *pReg = QgsProviderRegistry::instance(); + const QgsDataProvider::ProviderOptions providerOptions; + auto vectorProvider = std::unique_ptr( + qobject_cast< QgsVectorDataProvider * >( pReg->createProvider( "ogr", vsimemFilename, providerOptions ) ) ); + if ( !vectorProvider || !vectorProvider->isValid() ) + { + VSIUnlink( vsimemFilename.toUtf8().constData() ); + mErrorCode = QgsBaseNetworkRequest::ApplicationLevelError; + mAppLevelError = ApplicationLevelError::JsonError; + mErrorMessage = errorMessageWithReason( tr( "Loading of item failed" ) ); + emit gotResponse(); + return; + } + + mFields = vectorProvider->fields(); + auto iter = vectorProvider->getFeatures(); + iter.nextFeature( mFeature ); + vectorProvider.reset(); + VSIUnlink( vsimemFilename.toUtf8().constData() ); + + QgsDebugMsgLevel( QStringLiteral( "processReply end time: %1" ).arg( time( nullptr ) ), 5 ); + emit gotResponse(); +} diff --git a/src/providers/wfs/oapif/qgsoapifsingleitemrequest.h b/src/providers/wfs/oapif/qgsoapifsingleitemrequest.h new file mode 100644 index 000000000000..f8752ba2b47e --- /dev/null +++ b/src/providers/wfs/oapif/qgsoapifsingleitemrequest.h @@ -0,0 +1,74 @@ +/*************************************************************************** + qgsoapifsingleitemrequest.h + --------------------------- + begin : May 2024 + copyright : (C) 2024 by Even Rouault + email : even.rouault at spatialys.com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSOAPIFSINGLEITEMREQUEST_H +#define QGSOAPIFSINGLEITEMREQUEST_H + +#include + +#include "qgsdatasourceuri.h" +#include "qgsbasenetworkrequest.h" +#include "qgsfeature.h" + +//! Manages the /items/{id} request +class QgsOapifSingleItemRequest : public QgsBaseNetworkRequest +{ + Q_OBJECT + public: + explicit QgsOapifSingleItemRequest( const QgsDataSourceUri &uri, const QString &url ); + + //! Issue the request + bool request( bool synchronous, bool forceRefresh ); + + //! Application level error + enum class ApplicationLevelError + { + NoError, + JsonError, + IncompleteInformation + }; + + //! Returns application level error + ApplicationLevelError applicationLevelError() const { return mAppLevelError; } + + //! Return fields. + const QgsFields &fields() const { return mFields; } + + //! Return feature. + const QgsFeature &feature() const { return mFeature; } + + signals: + //! emitted when the capabilities have been fully parsed, or an error occurred + void gotResponse(); + + private slots: + void processReply(); + + protected: + QString errorMessageWithReason( const QString &reason ) override; + + private: + QString mUrl; + + bool mComputeBbox = false; + + QgsFields mFields; + + QgsFeature mFeature; + + ApplicationLevelError mAppLevelError = ApplicationLevelError::NoError; +}; + +#endif // QGSOAPIFSINGLEITEMREQUEST_H diff --git a/tests/src/python/test_provider_oapif.py b/tests/src/python/test_provider_oapif.py index 929d8b09c010..47b8d42d8f5b 100644 --- a/tests/src/python/test_provider_oapif.py +++ b/tests/src/python/test_provider_oapif.py @@ -1395,6 +1395,16 @@ def testFeatureInsertionDeletion(self): with open(sanitize(endpoint, '/collections/mycollection/items?POSTDATA={"geometry":null,"properties":{"cnt":null,"pk":null},"type":"Feature"}'), 'wb') as f: f.write(b"Location: /collections/mycollection/items/other_id\r\n") + new_id = {"type": "Feature", "id": "new_id", "properties": {"pk": 1, "cnt": 1234567890123}, + "geometry": {"type": "Point", "coordinates": [2, 49]}} + with open(sanitize(endpoint, '/collections/mycollection/items/new_id?' + ACCEPT_ITEMS), 'wb') as f: + f.write(json.dumps(new_id).encode('UTF-8')) + + other_id = {"type": "Feature", "id": "other_id", "properties": {"pk": 2, "cnt": 123}, + "geometry": None} + with open(sanitize(endpoint, '/collections/mycollection/items/other_id?' + ACCEPT_ITEMS), 'wb') as f: + f.write(json.dumps(other_id).encode('UTF-8')) + f = QgsFeature() f.setFields(vl.fields()) f.setAttributes([None, 1, 1234567890123]) @@ -1405,10 +1415,16 @@ def testFeatureInsertionDeletion(self): ret, fl = vl.dataProvider().addFeatures([f, f2]) self.assertTrue(ret) + self.assertEqual(fl[0].id(), 1) - self.assertEqual(fl[1].id(), 2) self.assertEqual(fl[0]["id"], "new_id") + self.assertEqual(fl[0]["pk"], 1) + self.assertEqual(fl[0]["cnt"], 1234567890123) + + self.assertEqual(fl[1].id(), 2) self.assertEqual(fl[1]["id"], "other_id") + self.assertEqual(fl[1]["pk"], 2) + self.assertEqual(fl[1]["cnt"], 123) # Failed attempt self.assertFalse(vl.dataProvider().deleteFeatures([1]))