Skip to content

Commit

Permalink
[OAPIF provider] Add support for edition capabilities (OGC API Featur…
Browse files Browse the repository at this point in the history
…es Part 4)

- Issues OPTIONS on /collections/{coll_id} and collections/{coll_id}/{feature_id}
  endpoints to check server capabilities
- Add support for addFeature() through POST /collections/{coll_id} endpoint
- Add support for changeGeometryValues()/changeAttributeValues() through
  PUT /collections/{coll_id}/{feature_id} endpoint.
  Or through PATCH /collections/{coll_id}/{feature_id} with just the
  updated subset, if available
- Add support for deleteFeatures() through
  DELETE /collections/{coll_id}/{feature_id} endpoint.
- For POST/PUT/PATCH, takes into account Part 2 (CRS support) when implemented
  by the server.
  • Loading branch information
rouault authored and nyalldawson committed Apr 18, 2023
1 parent efc0e5f commit 2924f3e
Show file tree
Hide file tree
Showing 18 changed files with 1,381 additions and 46 deletions.
5 changes: 5 additions & 0 deletions src/providers/wfs/CMakeLists.txt
Expand Up @@ -27,7 +27,12 @@ set(WFS_SRCS
oapif/qgsoapifapirequest.cpp
oapif/qgsoapifcollection.cpp
oapif/qgsoapifconformancerequest.cpp
oapif/qgsoapifcreatefeaturerequest.cpp
oapif/qgsoapifdeletefeaturerequest.cpp
oapif/qgsoapifpatchfeaturerequest.cpp
oapif/qgsoapifputfeaturerequest.cpp
oapif/qgsoapifitemsrequest.cpp
oapif/qgsoapifoptionsrequest.cpp
oapif/qgsoapifprovider.cpp
oapif/qgsoapifutils.cpp
)
Expand Down
82 changes: 82 additions & 0 deletions src/providers/wfs/oapif/qgsoapifcreatefeaturerequest.cpp
@@ -0,0 +1,82 @@
/***************************************************************************
qgsoapifcreatefeaturerequest.cpp
--------------------------------
begin : March 2023
copyright : (C) 2023 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 <nlohmann/json.hpp>
using namespace nlohmann;

#include "qgslogger.h"
#include "qgsjsonutils.h"
#include "qgsoapifcreatefeaturerequest.h"
#include "qgsoapifprovider.h"

QgsOapifCreateFeatureRequest::QgsOapifCreateFeatureRequest( const QgsDataSourceUri &uri ):
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
{
}

QString QgsOapifCreateFeatureRequest::createFeature( const QgsOapifSharedData *sharedData, const QgsFeature &f, const QString &contentCrs, bool hasAxisInverted )
{
QgsJsonExporter exporter;

QgsFeature fModified( f );
if ( hasAxisInverted && f.hasGeometry() )
{
QgsGeometry g = f.geometry();
g.get()->swapXy();
fModified.setGeometry( g );
}

json j = exporter.exportFeatureToJsonObject( fModified );
auto iterId = j.find( "id" );
if ( iterId != j.end() )
j.erase( iterId );
auto iterBbox = j.find( "bbox" );
if ( iterBbox != j.end() )
j.erase( iterBbox );
if ( !sharedData->mFoundIdInProperties && j["properties"].contains( "id" ) )
j["properties"].erase( "id" );
const QString jsonFeature = QString::fromStdString( j.dump() );
QgsDebugMsgLevel( jsonFeature, 5 );
mEmptyResponseIsValid = true;
mFakeResponseHasHeaders = true;
QList<QNetworkReply::RawHeaderPair> extraHeaders;
if ( !contentCrs.isEmpty() )
extraHeaders.append( QNetworkReply::RawHeaderPair( QByteArray( "Content-Crs" ), contentCrs.toUtf8() ) );
if ( !sendPOST( sharedData->mItemsUrl, "application/geo+json", jsonFeature.toUtf8(), extraHeaders ) )
return QString();

QString location;
for ( const auto &headerKeyValue : mResponseHeaders )
{
if ( headerKeyValue.first == QByteArray( "Location" ) )
{
location = QString::fromUtf8( headerKeyValue.second );
break;
}
}

const int posItems = location.lastIndexOf( QLatin1String( "/items/" ) );
if ( posItems < 0 )
return QString();

const QString createdId = location.mid( posItems + static_cast<int>( strlen( "/items/" ) ) );
QgsDebugMsgLevel( "createdId = " + createdId, 5 );
return createdId;
}

QString QgsOapifCreateFeatureRequest::errorMessageWithReason( const QString &reason )
{
return tr( "Create Feature request failed: %1" ).arg( reason );
}
41 changes: 41 additions & 0 deletions src/providers/wfs/oapif/qgsoapifcreatefeaturerequest.h
@@ -0,0 +1,41 @@
/***************************************************************************
qgsoapifcreatefeaturerequest.h
------------------------------
begin : March 2023
copyright : (C) 2023 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 QGSOAPIFCREATEFEATUREREQUEST_H
#define QGSOAPIFCREATEFEATUREREQUEST_H

#include <QObject>

#include "qgsdatasourceuri.h"
#include "qgsfeature.h"
#include "qgsbasenetworkrequest.h"

class QgsOapifSharedData;

//! Manages the Create Feature request
class QgsOapifCreateFeatureRequest : public QgsBaseNetworkRequest
{
Q_OBJECT
public:
explicit QgsOapifCreateFeatureRequest( const QgsDataSourceUri &uri );

//! Issue a POST request to create the feature and return its id
QString createFeature( const QgsOapifSharedData *sharedData, const QgsFeature &f, const QString &contentCrs, bool hasAxisInverted );

protected:
QString errorMessageWithReason( const QString &reason ) override;
};

#endif // QGSOAPIFCREATEFEATUREREQUEST_H
27 changes: 27 additions & 0 deletions src/providers/wfs/oapif/qgsoapifdeletefeaturerequest.cpp
@@ -0,0 +1,27 @@
/***************************************************************************
qgsoapifdeletefeaturerequest.cpp
--------------------------------
begin : March 2023
copyright : (C) 2023 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 "qgsoapifdeletefeaturerequest.h"

QgsOapifDeleteFeatureRequest::QgsOapifDeleteFeatureRequest( const QgsDataSourceUri &uri ):
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
{
}

QString QgsOapifDeleteFeatureRequest::errorMessageWithReason( const QString &reason )
{
return tr( "Delete Feature request failed: %1" ).arg( reason );
}
35 changes: 35 additions & 0 deletions src/providers/wfs/oapif/qgsoapifdeletefeaturerequest.h
@@ -0,0 +1,35 @@
/***************************************************************************
qgsoapifdeletefeaturerequest.h
------------------------------
begin : March 2023
copyright : (C) 2023 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 QGSOAPIFDELETEFEATUREREQUEST_H
#define QGSOAPIFDELETEFEATUREREQUEST_H

#include <QObject>

#include "qgsdatasourceuri.h"
#include "qgsbasenetworkrequest.h"

//! Manages the Delete Feature request
class QgsOapifDeleteFeatureRequest : public QgsBaseNetworkRequest
{
Q_OBJECT
public:
explicit QgsOapifDeleteFeatureRequest( const QgsDataSourceUri &uri );

protected:
QString errorMessageWithReason( const QString &reason ) override;
};

#endif // QGSOAPIFDELETEFEATUREREQUEST_H
9 changes: 9 additions & 0 deletions src/providers/wfs/oapif/qgsoapifitemsrequest.cpp
Expand Up @@ -133,6 +133,7 @@ void QgsOapifItemsRequest::processReply()
if ( jFeature.is_object() && jFeature.contains( "id" ) )
{
const json id = jFeature["id"];
mFoundIdTopLevel = true;
if ( id.is_string() )
{
mFeatures[i].second = QString::fromStdString( id.get<std::string>() );
Expand All @@ -142,6 +143,14 @@ void QgsOapifItemsRequest::processReply()
mFeatures[i].second = QString::number( id.get<qint64>() );
}
}
if ( jFeature.is_object() && jFeature.contains( "properties" ) )
{
const json properties = jFeature["properties"];
if ( properties.is_object() && properties.contains( "id" ) )
{
mFoundIdInProperties = true;
}
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/providers/wfs/oapif/qgsoapifitemsrequest.h
Expand Up @@ -68,6 +68,12 @@ class QgsOapifItemsRequest : public QgsBaseNetworkRequest
//! Return the url of the next page
const QString &nextUrl() const { return mNextUrl; }

//! Return if an "id" is present at top level of features
bool foundIdTopLevel() const { return mFoundIdTopLevel; }

//! Return if an "id" is present in the "properties" object of features
bool foundIdInProperties() const { return mFoundIdInProperties; }

signals:
//! emitted when the capabilities have been fully parsed, or an error occurred
void gotResponse();
Expand Down Expand Up @@ -97,6 +103,9 @@ class QgsOapifItemsRequest : public QgsBaseNetworkRequest

ApplicationLevelError mAppLevelError = ApplicationLevelError::NoError;

bool mFoundIdTopLevel = false;

bool mFoundIdInProperties = false;
};

#endif // QGSOAPIFITEMSREQUEST_H
26 changes: 26 additions & 0 deletions src/providers/wfs/oapif/qgsoapifoptionsrequest.cpp
@@ -0,0 +1,26 @@
/***************************************************************************
qgsoapifoptionsrequest.cpp
--------------------------
begin : March 2023
copyright : (C) 2023 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 "qgsoapifoptionsrequest.h"

QgsOapifOptionsRequest::QgsOapifOptionsRequest( const QgsDataSourceUri &uri ):
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
{
}

QString QgsOapifOptionsRequest::errorMessageWithReason( const QString &reason )
{
return tr( "Download of OPTIONS failed: %1" ).arg( reason );
}
35 changes: 35 additions & 0 deletions src/providers/wfs/oapif/qgsoapifoptionsrequest.h
@@ -0,0 +1,35 @@
/***************************************************************************
qgsoapifoptionsrequest.h
------------------------
begin : March 2023
copyright : (C) 2023 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 QGSOAPIFOPTIONSREQUEST_H
#define QGSOAPIFOPTIONSREQUEST_H

#include <QObject>

#include "qgsdatasourceuri.h"
#include "qgsbasenetworkrequest.h"

//! Manages the Options request
class QgsOapifOptionsRequest : public QgsBaseNetworkRequest
{
Q_OBJECT
public:
explicit QgsOapifOptionsRequest( const QgsDataSourceUri &uri );

protected:
QString errorMessageWithReason( const QString &reason ) override;
};

#endif // QGSOAPIFOPTIONSREQUEST_H
68 changes: 68 additions & 0 deletions src/providers/wfs/oapif/qgsoapifpatchfeaturerequest.cpp
@@ -0,0 +1,68 @@
/***************************************************************************
qgsoapifpatchfeaturerequest.cpp
-------------------------------
begin : March 2023
copyright : (C) 2023 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 <nlohmann/json.hpp>
using namespace nlohmann;

#include "qgsjsonutils.h"

#include "qgsoapifpatchfeaturerequest.h"
#include "qgsoapifprovider.h"

QgsOapifPatchFeatureRequest::QgsOapifPatchFeatureRequest( const QgsDataSourceUri &uri ):
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
{
}

bool QgsOapifPatchFeatureRequest::patchFeature( const QgsOapifSharedData *sharedData, const QString &jsonId, const QgsGeometry &geom, const QString &contentCrs, bool hasAxisInverted )
{
QgsGeometry geomModified( geom );
if ( hasAxisInverted )
{
geomModified.get()->swapXy();
}

json j;
j["geometry"] = geomModified.asJsonObject();
QList<QNetworkReply::RawHeaderPair> extraHeaders;
if ( !contentCrs.isEmpty() )
extraHeaders.append( QNetworkReply::RawHeaderPair( QByteArray( "Content-Crs" ), contentCrs.toUtf8() ) );
mEmptyResponseIsValid = true;
mFakeURLIncludesContentType = true;
QUrl url( sharedData->mItemsUrl + QString( QStringLiteral( "/" ) + jsonId ) );
return sendPATCH( url, "application/merge-patch+json", QString::fromStdString( j.dump() ).toUtf8(), extraHeaders );
}

bool QgsOapifPatchFeatureRequest::patchFeature( const QgsOapifSharedData *sharedData, const QString &jsonId, const QgsAttributeMap &attrMap )
{
json properties;
QgsAttributeMap::const_iterator attMapIt = attrMap.constBegin();
for ( ; attMapIt != attrMap.constEnd(); ++attMapIt )
{
QString fieldName = sharedData->mFields.at( attMapIt.key() ).name();
properties[ fieldName.toStdString() ] = QgsJsonUtils::jsonFromVariant( attMapIt.value() );
}
json j;
j[ "properties" ] = properties;
mEmptyResponseIsValid = true;
mFakeURLIncludesContentType = true;
QUrl url( sharedData->mItemsUrl + QString( QStringLiteral( "/" ) + jsonId ) );
return sendPATCH( url, "application/merge-patch+json", QString::fromStdString( j.dump() ).toUtf8() );
}

QString QgsOapifPatchFeatureRequest::errorMessageWithReason( const QString &reason )
{
return tr( "Patch Feature request failed: %1" ).arg( reason );
}

0 comments on commit 2924f3e

Please sign in to comment.