Skip to content

Commit

Permalink
[OAPIF provider] addFeatures(): issue a /items/{id} request to get a …
Browse files Browse the repository at this point in the history
…refreshed version of the feature

Fixes #57486
  • Loading branch information
rouault authored and nyalldawson committed May 23, 2024
1 parent b2bfa91 commit 91a5382
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/providers/wfs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ set(WFS_SRCS
oapif/qgsoapifoptionsrequest.cpp
oapif/qgsoapifprovider.cpp
oapif/qgsoapifqueryablesrequest.cpp
oapif/qgsoapifsingleitemrequest.cpp
oapif/qgsoapifutils.cpp
)

Expand Down
26 changes: 26 additions & 0 deletions src/providers/wfs/oapif/qgsoapifprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "qgsoapifitemsrequest.h"
#include "qgsoapifoptionsrequest.h"
#include "qgsoapifqueryablesrequest.h"
#include "qgsoapifsingleitemrequest.h"
#include "qgswfsconstants.h"
#include "qgswfsutils.h" // for isCompatibleType()

Expand Down Expand Up @@ -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();
Expand Down
106 changes: 106 additions & 0 deletions src/providers/wfs/oapif/qgsoapifsingleitemrequest.cpp
Original file line number Diff line number Diff line change
@@ -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 <QTextCodec>

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<GByte *>( reinterpret_cast<const GByte *>( buffer.constData() ) ),
buffer.size(),
false ) );
QgsProviderRegistry *pReg = QgsProviderRegistry::instance();
const QgsDataProvider::ProviderOptions providerOptions;
auto vectorProvider = std::unique_ptr<QgsVectorDataProvider>(
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();
}
74 changes: 74 additions & 0 deletions src/providers/wfs/oapif/qgsoapifsingleitemrequest.h
Original file line number Diff line number Diff line change
@@ -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 <QObject>

#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
18 changes: 17 additions & 1 deletion tests/src/python/test_provider_oapif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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]))
Expand Down

0 comments on commit 91a5382

Please sign in to comment.