QGIS/src/server/services/wfs3/qgswfs3handlers.cpp
2024-09-24 07:36:17 +02:00

2319 lines
86 KiB
C++

/***************************************************************************
qgswfs3handlers.cpp
-------------------------
begin : May 3, 2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 "qgswfs3handlers.h"
#include "qgsserverogcapi.h"
#include "qgsserverapicontext.h"
#include "qgsserverrequest.h"
#include "qgsserverresponse.h"
#include "qgsserverapiutils.h"
#include "qgsserverfeatureid.h"
#include "qgsfeaturerequest.h"
#include "qgsjsonutils.h"
#include "qgsogrutils.h"
#include "qgsvectorlayer.h"
#include "qgsmessagelog.h"
#include "qgsbufferserverrequest.h"
#include "qgsserverprojectutils.h"
#include "qgsserverinterface.h"
#include "qgsexpressioncontext.h"
#include "qgsexpressioncontextutils.h"
#include "qgsvectorlayerutils.h"
#include "qgslogger.h"
#include <QTextCodec>
#ifdef HAVE_SERVER_PYTHON_PLUGINS
#include "qgsfilterrestorer.h"
#include "qgsaccesscontrol.h"
#endif
QgsWfs3APIHandler::QgsWfs3APIHandler( const QgsServerOgcApi *api ):
mApi( api )
{
setContentTypes( { QgsServerOgcApi::ContentType::OPENAPI3, QgsServerOgcApi::ContentType::HTML } );
}
void QgsWfs3APIHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( ! context.project() )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Project not found, please check your server configuration." ) );
}
const QString contactPerson = QgsServerProjectUtils::owsServiceContactPerson( *context.project() );
const QString contactMail = QgsServerProjectUtils::owsServiceContactMail( *context.project() );
const QString projectTitle = QgsServerProjectUtils::owsServiceTitle( *context.project() );
const QString projectDescription = QgsServerProjectUtils::owsServiceAbstract( *context.project() );
const QgsProjectMetadata metadata { context.project()->metadata() };
json data
{
{ "openapi", "3.0.1" },
{
"tags", {{
{ "name", "Capabilities" },
{ "description", "Essential characteristics of this API including information about the data." }
}, {
{ "name", "Features" },
{ "description", "Access to data (features)." }
}
}
},
{
"info", {
{ "title", projectTitle.toStdString() },
{ "description", projectDescription.toStdString() },
{
"contact", {
{ "name", contactPerson.toStdString() },
{ "email", contactMail.toStdString() },
{ "url", "" } // TODO: contact url
}
},
{
"license", {
{ "name", "" } // TODO: license
}
},
{ "version", mApi->version().toStdString() }
}
},
{
"servers", {{
{ "url", parentLink( context.request()->url(), 1 ).toStdString() }
}
}
}
};
// Add links only if not OPENAPI3 to avoid validation errors
if ( QgsServerOgcApiHandler::contentTypeFromRequest( context.request() ) != QgsServerOgcApi::ContentType::OPENAPI3 )
{
data["links"] = links( context );
}
// Gather path information from handlers
json paths = json::array();
for ( const auto &h : mApi->handlers() )
{
// Skip null schema
const json hSchema = h->schema( context );
if ( ! hSchema.is_null() )
paths.merge_patch( hSchema );
}
data[ "paths" ] = paths;
// Schema: load common part from file schema.json
static json schema;
QFile f( context.serverInterface()->serverSettings()->apiResourcesDirectory() + "/ogc/schema.json" );
if ( f.open( QFile::ReadOnly | QFile::Text ) )
{
QTextStream in( &f );
schema = json::parse( in.readAll().toStdString() );
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "Could not find schema.json in %1, please check your server configuration" ).arg( f.fileName() ), QStringLiteral( "Server" ), Qgis::MessageLevel::Critical );
throw QgsServerApiInternalServerError( QStringLiteral( "Could not find schema.json" ) );
}
// Fill CRSs
json crss = json::array();
for ( const QString &crs : QgsServerApiUtils::publishedCrsList( context.project() ) )
{
crss.push_back( crs.toStdString() );
}
schema[ "components" ][ "parameters" ][ "bbox-crs" ][ "schema" ][ "enum" ] = crss;
schema[ "components" ][ "parameters" ][ "crs" ][ "schema" ][ "enum" ] = crss;
data[ "components" ] = schema["components"];
// Add schema refs
json navigation = json::array();
const QUrl url { context.request()->url() };
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 1 ).toStdString() }} ) ;
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", navigation }} );
}
json QgsWfs3APIHandler::schema( const QgsServerApiContext &context ) const
{
json data;
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/api" ), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", summary() },
{ "description", description() },
{ "operationId", operationId() },
{
"responses", {
{
"200", {
{ "description", description() },
{
"content", {
{
"application/vnd.oai.openapi+json;version=3.0", {
{
"schema", {
{ "type", "object" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
}
};
return data;
}
void QgsWfs3AbstractItemsHandler::checkLayerIsAccessible( QgsVectorLayer *mapLayer, const QgsServerApiContext &context ) const
{
const QVector<QgsVectorLayer *> publishedLayers = QgsServerApiUtils::publishedWfsLayers<QgsVectorLayer *>( context );
if ( ! publishedLayers.contains( mapLayer ) )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Collection was not found" ) );
}
}
QgsFeatureRequest QgsWfs3AbstractItemsHandler::filteredRequest( const QgsVectorLayer *vLayer, const QgsServerApiContext &context, const QStringList &subsetAttributes ) const
{
QgsFeatureRequest featureRequest;
QgsExpressionContext expressionContext;
expressionContext << QgsExpressionContextUtils::globalScope()
<< QgsExpressionContextUtils::projectScope( context.project() )
<< QgsExpressionContextUtils::layerScope( vLayer );
featureRequest.setExpressionContext( expressionContext );
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// Python plugins can make further modifications to the allowed attributes
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl )
{
accessControl->filterFeatures( vLayer, featureRequest );
}
#endif
QSet<QString> publishedAttrs;
const QgsFields constFields { publishedFields( vLayer, context ) };
for ( const QgsField &f : constFields )
{
if ( subsetAttributes.isEmpty() || subsetAttributes.contains( f.name( ) ) )
publishedAttrs.insert( f.name() );
}
featureRequest.setSubsetOfAttributes( publishedAttrs, vLayer->fields() );
return featureRequest;
}
QgsFields QgsWfs3AbstractItemsHandler::publishedFields( const QgsVectorLayer *vLayer, const QgsServerApiContext &context ) const
{
QStringList publishedAttributes = QStringList();
// Removed attributes
// WFS excluded attributes for this layer
const QgsFields &fields = vLayer->fields();
for ( const QgsField &field : fields )
{
if ( !field.configurationFlags().testFlag( Qgis::FieldConfigurationFlag::HideFromWfs ) )
{
publishedAttributes.push_back( field.name() );
}
}
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// Python plugins can make further modifications to the allowed attributes
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl )
{
publishedAttributes = accessControl->layerAttributes( vLayer, publishedAttributes );
}
#else
( void )context;
#endif
QgsFields publishedFields;
for ( int i = 0; i < fields.count(); ++i )
{
if ( publishedAttributes.contains( fields.at( i ).name() ) )
{
publishedFields.append( fields.at( i ) );
}
}
return publishedFields;
}
QgsWfs3LandingPageHandler::QgsWfs3LandingPageHandler()
{
}
void QgsWfs3LandingPageHandler::handleRequest( const QgsServerApiContext &context ) const
{
json data
{
{ "links", links( context ) }
};
// Append links to APIs
data["links"].push_back(
{
{ "href", href( context, "/collections" )},
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::data ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::JSON ) },
{ "title", "Feature collections" },
} );
data["links"].push_back(
{
{ "href", href( context, "/conformance" )},
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::conformance ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::JSON ) },
{ "title", "Conformance classes" },
} );
data["links"].push_back(
{
{ "href", href( context, "/api" )},
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::service_desc ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::OPENAPI3 ) },
{ "title", "API description" },
} );
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", json::array() }} );
}
json QgsWfs3LandingPageHandler::schema( const QgsServerApiContext &context ) const
{
json data;
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath(), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", summary() },
{ "description", description() },
{ "operationId", operationId() },
{
"responses", {
{
"200", {
{ "description", description() },
{
"content", {
{
"application/json", {
{
"schema", {
{ "$ref", "#/components/schemas/root" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
}
};
return data;
}
QgsWfs3ConformanceHandler::QgsWfs3ConformanceHandler()
{
}
void QgsWfs3ConformanceHandler::handleRequest( const QgsServerApiContext &context ) const
{
json data
{
{ "links", links( context ) },
{
"conformsTo", {
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html",
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson"
}
}
};
json navigation = json::array();
const QUrl url { context.request()->url() };
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 1 ).toStdString() }} ) ;
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", navigation }} );
}
json QgsWfs3ConformanceHandler::schema( const QgsServerApiContext &context ) const
{
json data;
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/conformance" ), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", summary() },
{ "description", description() },
{ "operationId", operationId() },
{
"responses", {
{
"200", {
{ "description", description() },
{
"content", {
{
"application/json", {
{
"schema", {
{ "$ref", "#/components/schemas/root" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
}
};
return data;
}
QgsWfs3CollectionsHandler::QgsWfs3CollectionsHandler()
{
}
void QgsWfs3CollectionsHandler::handleRequest( const QgsServerApiContext &context ) const
{
json crss = json::array();
for ( const QString &crs : QgsServerApiUtils::publishedCrsList( context.project() ) )
{
crss.push_back( crs.toStdString() );
}
json data
{
{
"links", links( context )
}, // TODO: add XSD or other schema?
{ "collections", json::array() },
{
"crs", crss
}
};
if ( context.project() )
{
const QgsProject *project = context.project();
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
for ( const QString &wfsLayerId : wfsLayerIds )
{
QgsVectorLayer *layer = qobject_cast<QgsVectorLayer *>( project->mapLayer( wfsLayerId ) );
if ( !layer )
{
continue;
}
if ( layer->type() != Qgis::LayerType::Vector )
{
continue;
}
try
{
// Check if the layer is published, raise not found if it is not
checkLayerIsAccessible( layer, context );
const std::string title{layer->serverProperties()->wfsTitle().isEmpty() ? layer->name().toStdString() : layer->serverProperties()->wfsTitle().toStdString()};
const QString shortName{layer->serverProperties()->shortName().isEmpty() ? layer->name() : layer->serverProperties()->shortName()};
data["collections"].push_back(
{
// identifier of the collection used, for example, in URIs
{ "id", shortName.toStdString() },
// human readable title of the collection
{ "title", title },
// a description of the features in the collection
{ "description", layer->serverProperties()->abstract().toStdString() },
{
"crs", crss
},
// TODO: "relations" ?
{
"extent", {
{
"spatial", {
{ "bbox", QgsServerApiUtils::layerExtent( layer ) },
{ "crs", "http://www.opengis.net/def/crs/OGC/1.3/CRS84" },
},
},
{
"temporal", {
{ "interval", QgsServerApiUtils::temporalExtent( layer ) },
{ "trs", "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" },
}
}
}
},
{
"links", {
{
{ "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::JSON ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::GEOJSON ) },
{ "title", title + " as GeoJSON" }
},
{
{ "href", href( context, QStringLiteral( "/%1/items" ).arg( shortName ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::HTML ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::HTML ) },
{ "title", title + " as HTML" }
}/* TODO: not sure what these "concepts" are about, neither if they are mandatory
{
{ "href", href( api, context.request(), QStringLiteral( "/%1/concepts" ).arg( shortName ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::item ) },
{ "type", "text/html" },
{ "title", "Describe " + title }
}
*/
}
},
} );
}
catch ( QgsServerApiNotFoundError & )
{
// Skip non-published layers
}
}
}
json navigation = json::array();
const QUrl url { context.request()->url() };
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 1 ).toStdString() }} ) ;
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", navigation }} );
}
json QgsWfs3CollectionsHandler::schema( const QgsServerApiContext &context ) const
{
json data;
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections" ), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", summary() },
{ "description", description() },
{ "operationId", operationId() },
{
"responses", {
{
"200", {
{ "description", description() },
{
"content", {
{
"application/json", {
{
"schema", {
{ "$ref", "#/components/schemas/content" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
}
};
return data;
}
QgsWfs3DescribeCollectionHandler::QgsWfs3DescribeCollectionHandler()
{
}
void QgsWfs3DescribeCollectionHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( ! context.project() )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Project is invalid or undefined" ) );
}
// Check collectionId
const QRegularExpressionMatch match { path().match( context.request()->url().path( ) ) };
if ( ! match.hasMatch() )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Collection was not found" ) );
}
const QString collectionId { match.captured( QStringLiteral( "collectionId" ) ) };
// May throw if not found
QgsVectorLayer *mapLayer { layerFromCollectionId( context, collectionId ) };
Q_ASSERT( mapLayer );
const QgsProject *project = context.project();
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
if ( ! wfsLayerIds.contains( mapLayer->id() ) )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Collection was not found" ) );
}
// Check if the layer is published, raise not found if it is not
checkLayerIsAccessible( mapLayer, context );
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
const std::string itemsTitle { title + " items" };
const QString shortName { mapLayer->serverProperties()->shortName().isEmpty() ? mapLayer->name() : mapLayer->serverProperties()->shortName() };
json linksList = links( context );
linksList.push_back(
{
{ "href", href( context, QStringLiteral( "/items" ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::JSON ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::GEOJSON ) },
{ "title", itemsTitle + " as " + QgsServerOgcApi::contentTypeToStdString( QgsServerOgcApi::ContentType::GEOJSON ) }
} );
linksList.push_back(
{
{ "href", href( context, QStringLiteral( "/items" ), QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::HTML ) ) },
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::items ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::HTML ) },
{ "title", itemsTitle + " as " + QgsServerOgcApi::contentTypeToStdString( QgsServerOgcApi::ContentType::HTML ) }
} );
linksList.push_back(
{
{
"href", parentLink( context.request()->url(), 3 ).toStdString() +
"?request=DescribeFeatureType&typenames=" +
QUrlQuery( shortName ).toString( QUrl::EncodeSpaces ).toStdString() +
"&service=WFS&version=2.0"
},
{ "rel", QgsServerOgcApi::relToString( QgsServerOgcApi::Rel::describedBy ) },
{ "type", QgsServerOgcApi::mimeType( QgsServerOgcApi::ContentType::XML ) },
{ "title", "Schema for " + title }
} );
json crss = json::array();
for ( const auto &crs : QgsServerApiUtils::publishedCrsList( context.project() ) )
{
crss.push_back( crs.toStdString() );
}
json data
{
{ "id", shortName.toStdString() },
{ "title", title },
// TODO: check if we need to expose other advertised CRS here
{
"crs", crss
},
// TODO: "relations" ?
{
"extent", {
{
"spatial", {
{ "bbox", QgsServerApiUtils::layerExtent( mapLayer ) },
{ "crs", "http://www.opengis.net/def/crs/OGC/1.3/CRS84" },
}
},
{
"temporal", {
{ "interval", QgsServerApiUtils::temporalExtent( mapLayer ) },
{ "trs", "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" },
}
}
}
},
{
"links", linksList
}
};
json navigation = json::array();
const QUrl url { context.request()->url() };
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 2 ).toStdString() }} ) ;
navigation.push_back( {{ "title", "Collections" }, { "href", parentLink( url, 1 ).toStdString() }} ) ;
write( data, context, {{ "pageTitle", title }, { "navigation", navigation }} );
}
json QgsWfs3DescribeCollectionHandler::schema( const QgsServerApiContext &context ) const
{
json data;
Q_ASSERT( context.project() );
const QVector<QgsVectorLayer *> layers { QgsServerApiUtils::publishedWfsLayers<QgsVectorLayer *>( context ) };
// Construct the context with collection id
for ( const auto &mapLayer : layers )
{
const QString shortName { mapLayer->serverProperties()->shortName().isEmpty() ? mapLayer->name() : mapLayer->serverProperties()->shortName() };
// Use layer id for operationId
const QString layerId { mapLayer->id() };
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections/%1" ).arg( shortName ), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", "Describe the '" + title + "' feature collection"},
{ "description", description() },
{ "operationId", operationId() + '_' + layerId.toStdString() },
{
"responses", {
{
"200", {
{ "description", "Metadata about the collection '" + title + "' shared by this API." },
{
"content", {
{
"application/json", {
{
"schema", {
{ "$ref", "#/components/schemas/collectionInfo" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
}
};
} // end for loop
return data;
}
QgsWfs3CollectionsItemsHandler::QgsWfs3CollectionsItemsHandler()
{
setContentTypes( { QgsServerOgcApi::ContentType::GEOJSON, QgsServerOgcApi::ContentType::HTML } );
}
QList<QgsServerQueryStringParameter> QgsWfs3CollectionsItemsHandler::parameters( const QgsServerApiContext &context ) const
{
QList<QgsServerQueryStringParameter> params;
// Limit
const qlonglong maxLimit { context.serverInterface()->serverSettings()->apiWfs3MaxLimit() };
QgsServerQueryStringParameter limit { QStringLiteral( "limit" ), false,
QgsServerQueryStringParameter::Type::Integer,
QStringLiteral( "Number of features to retrieve [0-%1]" ).arg( maxLimit ),
10 };
limit.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
bool ok = false;
const qlonglong longVal { value.toLongLong( &ok ) };
return ok && longVal >= 0 && longVal <= maxLimit;
} );
params.push_back( limit );
// Offset
QgsServerQueryStringParameter offset { QStringLiteral( "offset" ), false,
QgsServerQueryStringParameter::Type::Integer,
QStringLiteral( "Offset for features to retrieve [0-<number of features in the collection>]" ),
0 };
bool offsetValidatorSet = false;
// I'm not yet sure if we should get here without a project,
// but parameters() may be called to document the API - better safe than sorry.
if ( context.project() )
{
// Fields filters
const QgsVectorLayer *mapLayer { layerFromContext( context ) };
if ( mapLayer )
{
offset.setCustomValidator( [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
bool ok = false;
const qlonglong longVal { value.toLongLong( &ok ) };
return ok && longVal >= 0 && longVal <= mapLayer->featureCount( );
} );
offset.setDescription( QStringLiteral( "Offset for features to retrieve [0-%1]" ).arg( mapLayer->featureCount( ) ) );
offsetValidatorSet = true;
const QList<QgsServerQueryStringParameter> constFieldParameters { fieldParameters( mapLayer, context ) };
for ( const auto &p : constFieldParameters )
{
params.push_back( p );
}
// We want to accept both displayName and name.
const QgsFields published { publishedFields( mapLayer, context ) };
QStringList publishedFieldNames;
QStringList publishedFieldDisplayNames;
for ( const auto &f : published )
{
publishedFieldDisplayNames.push_back( f.displayName() );
if ( f.name() != f.displayName() )
{
publishedFieldNames.push_back( f.name() );
}
}
// Properties (CSV list of properties to return)
QgsServerQueryStringParameter properties { QStringLiteral( "properties" ), false,
QgsServerQueryStringParameter::Type::List,
QStringLiteral( "Comma separated list of feature property names to be added to the result. Valid values: %1" )
.arg( publishedFieldDisplayNames.join( QLatin1String( "', '" ) )
.append( '\'' )
.prepend( '\'' ) ) };
auto propertiesValidator = [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
const QStringList properties { value.toStringList() };
for ( const auto &p : properties )
{
if ( ! publishedFieldNames.contains( p ) && ! publishedFieldDisplayNames.contains( p ) )
{
return false;
}
}
return true;
};
properties.setCustomValidator( propertiesValidator );
params.push_back( properties );
}
// Check if is there any suitable datetime fields
if ( ! QgsServerApiUtils::temporalDimensions( mapLayer ).isEmpty() )
{
QgsServerQueryStringParameter datetime { QStringLiteral( "datetime" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "Datetime filter" ),
};
datetime.setCustomValidator( [ ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
const QString stringValue { value.toString() };
if ( stringValue.contains( '/' ) )
{
try
{
QgsServerApiUtils::parseTemporalDateInterval( stringValue );
}
catch ( QgsServerException & )
{
try
{
QgsServerApiUtils::parseTemporalDateTimeInterval( stringValue );
}
catch ( QgsServerException & )
{
return false;
}
}
}
else
{
if ( ! QDate::fromString( stringValue, Qt::DateFormat::ISODate ).isValid( ) &&
! QDateTime::fromString( stringValue, Qt::DateFormat::ISODate ).isValid( ) )
{
return false;
}
}
return true;
} );
params.push_back( datetime );
}
}
if ( ! offsetValidatorSet )
{
offset.setCustomValidator( [ ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
bool ok = false;
const qlonglong longVal { value.toLongLong( &ok ) };
return ok && longVal >= 0 ;
} );
}
params.push_back( offset );
// BBOX
const QgsServerQueryStringParameter bbox { QStringLiteral( "bbox" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "BBOX filter for the features to retrieve" ) };
params.push_back( bbox );
auto crsValidator = [ = ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
return QgsServerApiUtils::publishedCrsList( context.project() ).contains( value.toString() );
};
// BBOX CRS
QgsServerQueryStringParameter bboxCrs { QStringLiteral( "bbox-crs" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "CRS for the BBOX filter" ),
QStringLiteral( "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ) };
bboxCrs.setCustomValidator( crsValidator );
params.push_back( bboxCrs );
// CRS
QgsServerQueryStringParameter crs { QStringLiteral( "crs" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "The coordinate reference system of the response geometries." ),
QStringLiteral( "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ) };
crs.setCustomValidator( crsValidator );
params.push_back( crs );
// Result type
const QgsServerQueryStringParameter resultType { QStringLiteral( "resultType" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "Type of returned result: 'results' (default) or 'hits'" ),
QStringLiteral( "results" ) };
params.push_back( resultType );
// Sortby
const QgsServerQueryStringParameter sortBy { QStringLiteral( "sortby" ), false,
QgsServerQueryStringParameter::Type::String,
QStringLiteral( "Sort results by the specified field" )
};
params.push_back( sortBy );
// Sortdesc
const QgsServerQueryStringParameter sortDesc { QStringLiteral( "sortdesc" ), false,
QgsServerQueryStringParameter::Type::Boolean,
QStringLiteral( "Sort results in descending order, field name must be specified with 'sortby' parameter" ),
false };
params.push_back( sortDesc );
return params;
}
json QgsWfs3CollectionsItemsHandler::schema( const QgsServerApiContext &context ) const
{
json data;
Q_ASSERT( context.project() );
const QVector<QgsVectorLayer *> layers { QgsServerApiUtils::publishedWfsLayers<QgsVectorLayer *>( context ) };
// Construct the context with collection id
for ( const auto &mapLayer : layers )
{
const QString shortName { mapLayer->serverProperties()->shortName().isEmpty() ? mapLayer->name() : mapLayer->serverProperties()->shortName() };
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
// Use layer id for operationId
const QString layerId { mapLayer->id() };
const QString path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections/%1/items" ).arg( shortName ), context.request()->url() ) };
static const QStringList componentNames
{
QStringLiteral( "limit" ),
QStringLiteral( "offset" ),
QStringLiteral( "resultType" ),
QStringLiteral( "bbox" ),
QStringLiteral( "bbox-crs" ),
QStringLiteral( "crs" ),
QStringLiteral( "datetime" ),
QStringLiteral( "sortby" ),
QStringLiteral( "sortdesc" ),
};
json componentParameters = json::array();
for ( const QString &name : componentNames )
{
componentParameters.push_back( {{ "$ref", "#/components/parameters/" + name.toStdString() }} );
}
// Add layer specific filters
QgsServerApiContext layerContext( context );
QgsBufferServerRequest layerRequest( path );
layerContext.setRequest( &layerRequest );
const QList<QgsServerQueryStringParameter> requestParameters { parameters( layerContext ) };
for ( const auto &p : requestParameters )
{
if ( ! p.hidden() && ! componentNames.contains( p.name() ) )
componentParameters.push_back( p.data() );
}
data[ path.toStdString() ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", "Retrieve features of '" + title + "' feature collection" },
{ "description", description() },
{ "operationId", operationId() + '_' + layerId.toStdString() },
{ "parameters", componentParameters },
{
"responses", {
{
"200", {
{ "description", "Metadata about the collection '" + title + "' shared by this API." },
{
"content", {
{
"application/geo+json", {
{
"schema", {
{ "$ref", "#/components/schemas/featureCollectionGeoJSON" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
},
{
"post", {
{ "summary", "Adds a new feature to the collection {collectionId}" },
{ "tags", { "edit", "insert" } },
{ "description", "Adds a new feature to the collection {collectionId}" },
{ "operationId", operationId() + '_' + layerId.toStdString() + '_' + "POST" },
{
"responses", {
{
"201", {
{ "description", "A new feature was successfully added to the collection" }
},
},
{
"403", {
{ "description", "Forbidden: the operation requested was not authorized" }
},
},
{
"500", {
{ "description", "Posted data could not be parsed correctly or another error occurred" }
}
},
{ "default", defaultResponse() }
}
}
}
}
};
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
// If the layer has no insert capabilities, remove the post operation
if ( accessControl && !accessControl->layerInsertPermission( mapLayer ) )
{
data[ path.toStdString() ].erase( "post" );
}
#endif
} // end for loop
return data;
}
const QList<QgsServerQueryStringParameter> QgsWfs3CollectionsItemsHandler::fieldParameters( const QgsVectorLayer *mapLayer, const QgsServerApiContext &context ) const
{
QList<QgsServerQueryStringParameter> params;
if ( mapLayer )
{
const QgsFields constFields { publishedFields( mapLayer, context ) };
for ( const auto &f : constFields )
{
const QString fName { f.displayName() };
QgsServerQueryStringParameter::Type t;
switch ( f.type() )
{
case QMetaType::Type::Int:
case QMetaType::Type::LongLong:
t = QgsServerQueryStringParameter::Type::Integer;
break;
case QMetaType::Type::Double:
t = QgsServerQueryStringParameter::Type::Double;
break;
// TODO: date & time
default:
t = QgsServerQueryStringParameter::Type::String;
break;
}
const QgsServerQueryStringParameter fieldParam { fName, false,
t, QStringLiteral( "Retrieve features filtered by: %1 (%2)" ).arg( fName, QgsServerQueryStringParameter::typeName( t ) ) };
params.push_back( fieldParam );
// Add real field name if alias was used but set it as hidden
if ( fName != f.name() )
{
QgsServerQueryStringParameter fieldParam { f.name(), false,
t, QStringLiteral( "Retrieve features filtered by field: %1 (%2), aliased by %3" ).arg( f.name(), QgsServerQueryStringParameter::typeName( t ), f.alias() ) };
fieldParam.setHidden( true );
params.push_back( fieldParam );
}
}
}
return params;
}
void QgsWfs3CollectionsItemsHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( ! context.project() )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Project is invalid or undefined" ) );
}
QgsVectorLayer *mapLayer { layerFromContext( context ) };
Q_ASSERT( mapLayer );
// Check if the layer is published, raise not found if it is not
checkLayerIsAccessible( mapLayer, context );
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
// Get parameters
QVariantMap params = values( context );
switch ( context.request()->method() )
{
// //////////////////////////////////////////////////////////////
// Retrieve features
case QgsServerRequest::Method::GetMethod:
{
// Validate inputs
bool ok { false };
// BBOX
const QString bbox { params[ QStringLiteral( "bbox" )].toString() };
const QgsRectangle filterRect { QgsServerApiUtils::parseBbox( bbox ) };
if ( ! bbox.isEmpty() && filterRect.isNull() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "bbox is not valid" ) );
}
// BBOX CRS
const QgsCoordinateReferenceSystem bboxCrs { QgsServerApiUtils::parseCrs( params[ QStringLiteral( "bbox-crs" ) ].toString() ) };
if ( ! bboxCrs.isValid() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "BBOX CRS is not valid" ) );
}
// CRS
const QgsCoordinateReferenceSystem crs { QgsServerApiUtils::parseCrs( params[ QStringLiteral( "crs" ) ].toString() ) };
if ( ! crs.isValid() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "CRS is not valid" ) );
}
// resultType
const QString resultType { params[ QStringLiteral( "resultType" ) ].toString() };
static const QStringList availableResultTypes { QStringLiteral( "results" ), QStringLiteral( "hits" )};
if ( ! availableResultTypes.contains( resultType ) )
{
throw QgsServerApiBadRequestException( QStringLiteral( "resultType is not valid [results, hits]" ) );
}
// Attribute filters
QgsStringMap attrFilters;
const QgsFields constPublishedFields { publishedFields( mapLayer, context ) };
for ( const QgsField &f : constPublishedFields )
{
QString val = params.value( f.name() ).toString();
// Try alias
if ( val.isEmpty() && ! f.alias().isEmpty() )
{
val = params.value( f.alias() ).toString();
}
if ( ! val.isEmpty() )
{
const QString sanitized { QgsServerApiUtils::sanitizedFieldValue( val ) };
if ( sanitized.isEmpty() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Invalid filter field value [%1=%2]" ).arg( f.name(), val ) );
}
attrFilters[f.name()] = sanitized;
}
}
// limit & offset
// Apparently the standard set limits 0-10000 (and does not implement paging,
// so we do our own paging with "offset")
const qlonglong offset { params.value( QStringLiteral( "offset" ) ).toLongLong( &ok ) };
const qlonglong limit { params.value( QStringLiteral( "limit" ) ).toLongLong( &ok ) };
QString filterExpression;
QStringList expressions;
// datetime
const QString datetime { params.value( QStringLiteral( "datetime" ) ).toString() };
if ( ! datetime.isEmpty() )
{
const QgsExpression timeExpression { QgsServerApiUtils::temporalFilterExpression( mapLayer, datetime ) };
if ( ! timeExpression.isValid() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Invalid datetime filter expression: %1 " ).arg( datetime ) );
}
else
{
expressions.push_back( timeExpression.expression() );
}
}
// Properties (subset attributes)
const QStringList inputRequestedProperties { params.value( QStringLiteral( "properties" ) ).toStringList( ) };
// Cleanup (may throw)
QStringList requestedProperties;
for ( const QString &property : std::as_const( inputRequestedProperties ) )
{
requestedProperties.push_back( QgsServerApiUtils::fieldName( QgsServerApiUtils::sanitizedFieldValue( property ), mapLayer ) );
}
// Sorting
const QString sortBy { params.value( QStringLiteral( "sortby" ) ).toString( ) };
const bool sortDesc { params.value( QStringLiteral( "sortdesc" ) ).toBool( ) };
if ( !sortBy.isEmpty() )
{
// fieldName may throw a different message ...
try
{
if ( ! constPublishedFields.names().contains( QgsServerApiUtils::fieldName( QgsServerApiUtils::sanitizedFieldValue( sortBy ), mapLayer ) ) )
{
throw QgsServerApiBadRequestException( QString() );
}
}
catch ( const QgsServerApiBadRequestException & )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Invalid sortBy field '%1'" ).arg( QgsServerApiUtils::sanitizedFieldValue( sortBy ) ) );
}
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////
// End of input control: inputs are valid, process the request
QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context, requestedProperties );
if ( ! sortBy.isEmpty() )
{
featureRequest.setOrderBy( { { { sortBy, ! sortDesc } } } );
}
if ( ! filterRect.isNull() )
{
const QgsCoordinateTransform ct( bboxCrs, crs, context.project()->transformContext() );
try
{
featureRequest.setFilterRect( ct.transform( filterRect ) );
}
catch ( QgsCsException & )
{
throw QgsServerApiInternalServerError( QStringLiteral( "BBOX CRS could not be transformed to destination CRS" ) );
}
}
if ( ! attrFilters.isEmpty() )
{
if ( featureRequest.filterExpression() && ! featureRequest.filterExpression()->expression().isEmpty() )
{
expressions.push_back( featureRequest.filterExpression()->expression() );
}
for ( auto it = attrFilters.constBegin(); it != attrFilters.constEnd(); it++ )
{
// Handle star
static const QRegularExpression re2( R"raw([^\\]\*)raw" );
if ( re2.match( it.value() ).hasMatch() )
{
QString val { it.value() };
expressions.push_back( QStringLiteral( "\"%1\" LIKE '%2'" ).arg( it.key() ).arg( val.replace( '%', QLatin1String( "%%" ) ).replace( '*', '%' ) ) );
}
else
{
expressions.push_back( QStringLiteral( "\"%1\" = '%2'" ).arg( it.key() ).arg( it.value() ) );
}
}
}
// Join all expression filters
if ( ! expressions.isEmpty() )
{
filterExpression = expressions.join( QLatin1String( " AND " ) );
featureRequest.setFilterExpression( filterExpression );
QgsDebugMsgLevel( QStringLiteral( "Filter expression: %1" ).arg( featureRequest.filterExpression()->expression() ), 4 );
}
// WFS3 core specs only serves 4326
featureRequest.setDestinationCrs( crs, context.project()->transformContext() );
// Add offset to limit because paging is not supported by QgsFeatureRequest
featureRequest.setLimit( limit + offset );
QgsJsonExporter exporter { mapLayer };
exporter.setAttributes( featureRequest.subsetOfAttributes() );
exporter.setAttributeDisplayName( true );
exporter.setSourceCrs( mapLayer->crs() );
exporter.setTransformGeometries( false );
QgsFeatureList featureList;
QgsFeatureIterator features { mapLayer->getFeatures( featureRequest ) };
QgsFeature feat;
long i { 0 };
QMap<QgsFeatureId, QString> fidMap;
while ( features.nextFeature( feat ) )
{
// Ignore records before offset
if ( i >= offset )
{
fidMap.insert( feat.id(), QgsServerFeatureId::getServerFid( feat, mapLayer->dataProvider()->pkAttributeIndexes() ) );
featureList << feat;
}
i++;
}
// Count features
long matchedFeaturesCount = 0;
if ( attrFilters.isEmpty() && filterRect.isNull() )
{
matchedFeaturesCount = mapLayer->featureCount();
}
else
{
if ( filterExpression.isEmpty() )
{
featureRequest.setNoAttributes();
}
featureRequest.setFlags( Qgis::FeatureRequestFlag::NoGeometry );
featureRequest.setLimit( -1 );
features = mapLayer->getFeatures( featureRequest );
while ( features.nextFeature( feat ) )
{
matchedFeaturesCount++;
}
}
json data = exporter.exportFeaturesToJsonObject( featureList );
// Patch feature IDs with server feature IDs
for ( int i = 0; i < featureList.length(); i++ )
{
data[ "features" ][ i ]["id"] = fidMap.value( data[ "features" ][ i ]["id"] ).toStdString();
}
// Add some metadata
data["numberMatched"] = matchedFeaturesCount;
data["numberReturned"] = featureList.count();
data["links"] = links( context );
// Current url
const QUrl url { context.request()->url() };
// Url without offset and limit
QUrl cleanedUrl { url };
QUrlQuery query( cleanedUrl );
query.removeQueryItem( QStringLiteral( "limit" ) );
query.removeQueryItem( QStringLiteral( "offset" ) );
cleanedUrl.setQuery( query );
QString cleanedUrlAsString { cleanedUrl.toString() };
if ( ! cleanedUrl.hasQuery() )
{
cleanedUrlAsString += '?';
}
else
{
cleanedUrlAsString += '&';
}
// Pagesize metadata
json pagesize = json::array();
const qlonglong maxLimit { context.serverInterface()->serverSettings()->apiWfs3MaxLimit() };
if ( matchedFeaturesCount > 1 && maxLimit > 1 )
{
const std::string pageSizeOneLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=1" ).toStdString() };
pagesize.push_back( {{ "title", "1" }, { "href", pageSizeOneLink }} ) ;
if ( matchedFeaturesCount > 10 && maxLimit > 10 )
{
const std::string pageSizeTenLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=10" ).toStdString() };
pagesize.push_back( {{ "title", "10" }, { "href", pageSizeTenLink }} ) ;
}
if ( matchedFeaturesCount > 20 && maxLimit > 20 )
{
const std::string pageSizeTwentyLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=20" ).toStdString() };
pagesize.push_back( {{ "title", "20" }, { "href", pageSizeTwentyLink }} ) ;
}
if ( matchedFeaturesCount > 50 && maxLimit > 50 )
{
const std::string pageSizeFiftyLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=50" ).toStdString() };
pagesize.push_back( {{ "title", "50" }, { "href", pageSizeFiftyLink }} ) ;
}
if ( matchedFeaturesCount > 100 && maxLimit > 100 )
{
const std::string pageSizeHundredLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=100" ).toStdString() };
pagesize.push_back( {{ "title", "100" }, { "href", pageSizeHundredLink }} ) ;
}
if ( matchedFeaturesCount > 1000 && maxLimit > 1000 )
{
const std::string pageSizeThousandLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=1000" ).toStdString() };
pagesize.push_back( {{ "title", "1000" }, { "href", pageSizeThousandLink }} ) ;
}
std::string maxTitle = "All";
if ( maxLimit < matchedFeaturesCount )
{
maxTitle = "Maximum";
}
const std::string pageSizeMaxLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=%1" ).arg( maxLimit ).toStdString() };
pagesize.push_back( {{ "title", maxTitle }, { "href", pageSizeMaxLink }} ) ;
}
// Get the self link
json selfLink;
for ( const auto &l : data["links"] )
{
if ( l["rel"] == "self" )
{
selfLink = l;
break;
}
}
// Pagination metadata
json pagination = json::array();
if ( limit != 0 )
{
// Add prev - next links
json prevLink;
if ( offset != 0 )
{
prevLink = selfLink;
prevLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::max<long>( 0, offset - limit ) ).arg( limit ).toStdString();
prevLink["rel"] = "prev";
prevLink["title"] = "Previous page";
data["links"].push_back( prevLink );
}
json nextLink;
if ( limit + offset < matchedFeaturesCount )
{
nextLink = selfLink;
nextLink["href"] = cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( std::min<long>( matchedFeaturesCount, limit + offset ) ).arg( limit ).toStdString();
nextLink["rel"] = "next";
nextLink["title"] = "Next page";
data["links"].push_back( nextLink );
}
// Pagination
if ( matchedFeaturesCount - limit > 0 )
{
const int totalPages { static_cast<int>( std::ceil( static_cast<float>( matchedFeaturesCount ) / static_cast<float>( limit ) ) ) };
const int currentPage { static_cast<int>( offset / limit + 1 ) };
const std::string currentPageLink { selfLink["href"] };
std::string prevPageLink;
if ( prevLink.contains( std::string{ "href" } ) )
{
prevPageLink = prevLink["href"];
}
std::string nextPageLink;
if ( nextLink.contains( std::string{ "href" } ) )
{
nextPageLink = nextLink["href"];
}
const std::string firstPageLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=0&limit=%1" ).arg( limit ).toStdString() };
const std::string lastPageLink { cleanedUrlAsString.toStdString() + QStringLiteral( "offset=%1&limit=%2" ).arg( totalPages * limit - limit ).arg( limit ).toStdString() };
if ( currentPage != 1 )
{
pagination.push_back( {{ "title", "1" }, { "href", firstPageLink }, { "class", "page-item" }} ) ;
}
if ( currentPage > 3 )
{
pagination.push_back( {{ "title", "\u2026" }, { "class", "page-item disabled" }} ) ;
}
if ( currentPage > 2 )
{
pagination.push_back( {{ "title", std::to_string( currentPage - 1 ) }, { "href", prevPageLink }, { "class", "page-item" }} ) ;
}
pagination.push_back( {{ "title", std::to_string( currentPage ) }, { "href", currentPageLink }, { "class", "page-item active" }} ) ;
if ( currentPage < totalPages - 1 )
{
pagination.push_back( {{ "title", std::to_string( currentPage + 1 ) }, { "href", nextPageLink }, { "class", "page-item" }} ) ;
}
if ( currentPage < totalPages - 2 )
{
pagination.push_back( {{ "title", "\u2026" }, { "class", "page-item disabled" }} ) ;
}
if ( currentPage != totalPages )
{
pagination.push_back( {{ "title", std::to_string( totalPages ) }, { "href", lastPageLink }, { "class", "page-item" }} ) ;
}
// Add first - last links
// Since we are having them ready, not mandatory by the spec but allowed
json firstLink = selfLink;
firstLink["href"] = firstPageLink;
firstLink["rel"] = "first";
firstLink["title"] = "First page";
data["links"].push_back( firstLink );
json lastLink = selfLink;
lastLink["href"] = lastPageLink;
lastLink["rel"] = "last";
lastLink["title"] = "Last page";
data["links"].push_back( lastLink );
}
}
json navigation = json::array();
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 3 ).toStdString() }} ) ;
navigation.push_back( {{ "title", "Collections" }, { "href", parentLink( url, 2 ).toStdString() }} ) ;
navigation.push_back( {{ "title", title }, { "href", parentLink( url, 1 ).toStdString() }} ) ;
const json htmlMetadata
{
{ "pageTitle", "Features in layer " + title },
{ "layerTitle", title },
{
"geojsonUrl", href( context, "/",
QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::GEOJSON ) )
},
{ "pagesize", pagesize },
{ "pagination", pagination },
{ "navigation", navigation }
};
write( data, context, htmlMetadata );
break;
}
// //////////////////////////////////////////////////////////////
// Create a new feature
case QgsServerRequest::Method::PostMethod:
{
// First: check permissions
const QStringList wfstInsertLayerIds = QgsServerProjectUtils::wfstInsertLayerIds( *context.project() );
if ( ! wfstInsertLayerIds.contains( mapLayer->id() ) || ! mapLayer->dataProvider()->capabilities().testFlag( Qgis::VectorProviderCapability::AddFeatures ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Features cannot be added to layer '%1'" ).arg( mapLayer->name() ) );
}
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl && !accessControl->layerInsertPermission( mapLayer ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "No ACL permissions to insert features on layer '%1'" ).arg( mapLayer->name() ) );
}
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
if ( accessControl )
{
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
}
#endif
try
{
// Parse
json postData = json::parse( context.request()->data().toStdString() );
// Process data: extract geometry (because we need to process attributes in a much more complex way)
const QgsFields fields = QgsOgrUtils::stringToFields( context.request()->data(), QTextCodec::codecForName( "UTF-8" ) );
const QgsFeatureList features = QgsOgrUtils::stringToFeatureList( context.request()->data(), fields, QTextCodec::codecForName( "UTF-8" ) );
if ( features.isEmpty() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Posted data does not contain any feature" ) );
}
QgsFeature feat = features.first();
if ( ! feat.isValid() )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Feature is not valid" ) );
}
// Transform geometry
if ( mapLayer->crs() != QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) )
{
QgsGeometry geom { feat.geometry() };
try
{
geom.transform( QgsCoordinateTransform( QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), mapLayer->crs(), context.project()->transformContext() ) );
}
catch ( QgsCsException & )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Geometry could not be transformed to destination CRS" ) );
}
feat.setGeometry( geom );
}
// Process attributes
try
{
const QgsFields authorizedFields { publishedFields( mapLayer, context ) };
QStringList authorizedFieldNames;
for ( const auto &f : authorizedFields )
{
authorizedFieldNames.push_back( f.name() );
}
const QVariantMap properties = QgsJsonUtils::parseJson( postData["properties"].dump( ) ).toMap( );
const QgsFields fields = mapLayer->fields();
for ( const auto &field : fields )
{
if ( ! QgsVariantUtils::isNull( properties.value( field.name() ) ) )
{
if ( ! authorizedFieldNames.contains( field.name() ) )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Feature field %1 is not allowed" ).arg( field.name() ) );
}
else
{
QVariant value = properties.value( field.name() );
// Convert blobs
if ( !QgsVariantUtils::isNull( properties.value( field.name() ) ) && static_cast<QMetaType::Type>( field.type() ) == QMetaType::QByteArray )
{
value = QByteArray::fromBase64( value.toByteArray() );
}
feat.setAttribute( field.name(), value );
}
}
else
{
feat.setAttribute( field.name(), QVariant() );
}
}
}
catch ( json::exception & )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Feature properties are not valid" ) );
}
// Make sure the first field (id) is null for shapefiles
if ( mapLayer->providerType() == QLatin1String( "ogr" ) && mapLayer->storageType() == QLatin1String( "ESRI Shapefile" ) )
{
feat.setAttribute( 0, QVariant() );
}
feat.setId( FID_NULL );
QgsVectorLayerUtils::matchAttributesToFields( feat, mapLayer->fields( ) );
QgsFeatureList featuresToAdd( { feat } );
if ( ! mapLayer->dataProvider()->addFeatures( featuresToAdd ) )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Error adding feature to collection" ) );
}
feat = featuresToAdd.first();
// Send response
context.response()->setStatusCode( 201 );
context.response()->setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "application/geo+json" ) );
QString url { context.request()->url().toString( QUrl::EncodeSpaces ) };
if ( ! url.endsWith( '/' ) )
{
url.append( '/' );
}
context.response()->setHeader( QStringLiteral( "Location" ), url + QString::number( feat.id() ) );
context.response()->write( "\"string\"" );
}
catch ( json::exception &ex )
{
throw QgsServerApiBadRequestException( QStringLiteral( "JSON parse error: %1" ).arg( ex.what( ) ) );
}
break;
}
// Error
default:
{
throw QgsServerApiNotImplementedException( QStringLiteral( "%1 method is not implemented." )
.arg( QgsServerRequest::methodToString( context.request()->method() ) ) );
}
} // end switch
}
QgsWfs3CollectionsFeatureHandler::QgsWfs3CollectionsFeatureHandler()
{
setContentTypes( { QgsServerOgcApi::ContentType::GEOJSON, QgsServerOgcApi::ContentType::HTML } );
}
void QgsWfs3CollectionsFeatureHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( ! context.project() )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Project is invalid or undefined" ) );
}
// Check collectionId
const QRegularExpressionMatch match { path().match( context.request()->url().path( ) ) };
if ( ! match.hasMatch() )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Collection was not found" ) );
}
const QString collectionId { match.captured( QStringLiteral( "collectionId" ) ) };
// May throw if not found
QgsVectorLayer *mapLayer { layerFromCollectionId( context, collectionId ) };
Q_ASSERT( mapLayer );
// Check if the layer is published, raise not found if it is not
checkLayerIsAccessible( mapLayer, context );
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
// Retrieve feature from storage
const QString featureId { match.captured( QStringLiteral( "featureId" ) ) };
QgsFeatureRequest featureRequest = filteredRequest( mapLayer, context );
const QString fidExpression { QgsServerFeatureId::getExpressionFromServerFid( featureId, mapLayer->dataProvider() ) };
if ( ! fidExpression.isEmpty() )
{
QgsExpression *filterExpression { featureRequest.filterExpression() };
if ( ! filterExpression )
{
featureRequest.setFilterExpression( fidExpression );
}
else
{
featureRequest.setFilterExpression( QStringLiteral( "(%1) AND (%2)" ).arg( fidExpression, filterExpression->expression() ) );
}
}
else
{
bool ok;
featureRequest.setFilterFid( featureId.toLongLong( &ok ) );
if ( ! ok )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Invalid feature ID [%1]" ).arg( featureId ) );
}
}
QgsFeature feature;
QgsFeatureIterator it { mapLayer->getFeatures( featureRequest ) };
if ( ! it.nextFeature( feature ) || ! feature.isValid() )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Invalid feature [%1]" ).arg( featureId ) );
}
auto doGet = [ & ]( )
{
#ifdef HAVE_SERVER_PYTHON_PLUGINS
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
if ( accessControl )
{
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
}
#endif
QgsJsonExporter exporter { mapLayer };
exporter.setAttributes( featureRequest.subsetOfAttributes() );
exporter.setAttributeDisplayName( true );
json data = exporter.exportFeatureToJsonObject( feature );
// Patch feature ID
data["id"] = featureId.toStdString();
data["links"] = links( context );
json navigation = json::array();
const QUrl url { context.request()->url() };
navigation.push_back( {{ "title", "Landing page" }, { "href", parentLink( url, 4 ).toStdString() }} ) ;
navigation.push_back( {{ "title", "Collections" }, { "href", parentLink( url, 3 ).toStdString() }} ) ;
navigation.push_back( {{ "title", title }, { "href", parentLink( url, 2 ).toStdString() }} ) ;
navigation.push_back( {{ "title", "Items of " + title }, { "href", parentLink( url ).toStdString() }} ) ;
const json htmlMetadata
{
{ "pageTitle", title + " - feature " + featureId.toStdString() },
{
"geojsonUrl", href( context, "",
QgsServerOgcApi::contentTypeToExtension( QgsServerOgcApi::ContentType::GEOJSON ) )
},
{ "navigation", navigation }
};
write( data, context, htmlMetadata );
};
switch ( context.request()->method() )
{
// //////////////////////////////////////////////////////////////
// Retrieve a single feature
case QgsServerRequest::Method::GetMethod:
{
doGet();
break;
}
// //////////////////////////////////////////////////////////////
// Replace feature, PATCH should be used for partial updates but we allow partial updates here too
// because according to the specs PATCH does not allow changes to the geometry.
// TODO: factor with items handler POST, that uses mostly the same code
// QUESTION: do we want make things easier for clients and also allow POST here?
case QgsServerRequest::Method::PostMethod:
case QgsServerRequest::Method::PutMethod:
{
// First: check permissions
const QStringList wfstUpdateLayerIds = QgsServerProjectUtils::wfstUpdateLayerIds( *context.project() );
if ( ! wfstUpdateLayerIds.contains( mapLayer->id() ) ||
! mapLayer->dataProvider()->capabilities().testFlag( Qgis::VectorProviderCapability::ChangeGeometries ) ||
! mapLayer->dataProvider()->capabilities().testFlag( Qgis::VectorProviderCapability::ChangeAttributeValues ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Features in layer '%1' cannot be changed" ).arg( mapLayer->name() ) );
}
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl && !accessControl->layerUpdatePermission( mapLayer ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "No ACL permissions to change features on layer '%1'" ).arg( mapLayer->name() ) );
}
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
if ( accessControl )
{
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
}
#endif
try
{
// Parse
json postData = json::parse( context.request()->data().toStdString() );
// Process data: extract geometry (because we need to process attributes in a much more complex way)
const QgsFields fields( QgsOgrUtils::stringToFields( context.request()->data(), QTextCodec::codecForName( "UTF-8" ) ) );
const QgsFeatureList features = QgsOgrUtils::stringToFeatureList( context.request()->data(), fields, QTextCodec::codecForName( "UTF-8" ) );
if ( features.isEmpty() )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Posted data does not contain any feature" ) );
}
const QgsFeature feat = features.first();
if ( ! feat.isValid() )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Feature is not valid" ) );
}
QgsChangedAttributesMap changedAttributes;
QgsAttributeMap changedMap;
QgsGeometryMap changedGeometries;
// Transform geometry
if ( mapLayer->crs() != QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) )
{
QgsGeometry geom { feat.geometry() };
try
{
geom.transform( QgsCoordinateTransform( QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), mapLayer->crs(), context.project()->transformContext() ) );
}
catch ( QgsCsException & )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Geometry could not be transformed to destination CRS" ) );
}
changedGeometries.insert( feature.id(), geom );
}
// Process attributes
try
{
const QgsFields authorizedFields { publishedFields( mapLayer, context ) };
QStringList authorizedFieldNames;
for ( const auto &f : authorizedFields )
{
authorizedFieldNames.push_back( f.name() );
}
const QVariantMap properties = QgsJsonUtils::parseJson( postData["properties"].dump( ) ).toMap( );
const QgsFields fields = mapLayer->fields();
int fieldIndex = 0;
for ( const auto &field : fields )
{
if ( ! QgsVariantUtils::isNull( properties.value( field.name() ) ) )
{
if ( ! authorizedFieldNames.contains( field.name() ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Feature field '%1' change is not allowed" ).arg( field.name() ) );
}
else
{
QVariant value = properties.value( field.name() );
// Convert blobs
if ( ! QgsVariantUtils::isNull( properties.value( field.name() ) ) && static_cast<QMetaType::Type>( field.type() ) == QMetaType::QByteArray )
{
value = QByteArray::fromBase64( value.toByteArray() );
}
changedMap.insert( fieldIndex, value );
}
}
else
{
// We don't want to set NULL here, in case of partial updates (not sure yet about what the specs will say about this case)
// changedMap.insert( fieldIndex, QVariant( ) );
}
fieldIndex++;
}
if ( ! changedMap.isEmpty() )
{
changedAttributes.insert( feature.id(), changedMap );
}
}
catch ( json::exception & )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Feature properties are not valid" ) );
}
// TODO: raise if nothing to change?
if ( ! mapLayer->dataProvider()->changeFeatures( changedAttributes, changedGeometries ) )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Error changing feature" ) );
}
// Now we need to send the updated feature to the client
feature = mapLayer->getFeature( feature.id() );
doGet();
}
catch ( json::exception &ex )
{
throw QgsServerApiBadRequestException( QStringLiteral( "JSON parse error: %1" ).arg( ex.what( ) ) );
}
break;
}
// //////////////////////////////////////////////////////////////
// Patch feature
case QgsServerRequest::Method::PatchMethod:
{
// First: check permissions
const QStringList wfstUpdateLayerIds = QgsServerProjectUtils::wfstUpdateLayerIds( *context.project() );
if ( ! wfstUpdateLayerIds.contains( mapLayer->id() ) ||
! mapLayer->dataProvider()->capabilities().testFlag( Qgis::VectorProviderCapability::ChangeAttributeValues ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Feature attributes in layer '%1' cannot be changed" ).arg( mapLayer->name() ) );
}
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl && !accessControl->layerUpdatePermission( mapLayer ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "No ACL permissions to change features on layer '%1'" ).arg( mapLayer->name() ) );
}
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
if ( accessControl )
{
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
}
#endif
QgsChangedAttributesMap changedAttributes;
QgsAttributeMap changedMap;
const QgsGeometryMap changedGeometries; // This will be empty
try
{
// Parse
json postData = json::parse( context.request()->data().toStdString() );
// If the request contains "add" we raise
if ( postData.contains( "add" ) )
{
throw QgsServerApiNotImplementedException( QStringLiteral( "\"add\" instruction in PATCH method is not implemented" ),
QString::fromStdString( QgsServerOgcApi::mimeType( contentTypeFromRequest( context.request() ) ) ),
400 );
}
// If the request does NOT contain "modify" we raise
if ( ! postData.contains( "modify" ) )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Missing \"modify\" instruction in PATCH method" ) );
}
// Process attributes
try
{
const QgsFields authorizedFields { publishedFields( mapLayer, context ) };
QStringList authorizedFieldNames;
for ( const auto &f : authorizedFields )
{
authorizedFieldNames.push_back( f.name() );
}
const QVariantMap properties = QgsJsonUtils::parseJson( postData["modify"].dump( ) ).toMap( );
const QgsFields fields = mapLayer->fields();
int fieldIndex = 0;
for ( const auto &field : fields )
{
if ( ! QgsVariantUtils::isNull( properties.value( field.name() ) ) )
{
if ( ! authorizedFieldNames.contains( field.name() ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Feature field '%1' change is not allowed" ).arg( field.name() ) );
}
else
{
QVariant value = properties.value( field.name() );
// Convert blobs
if ( ! QgsVariantUtils::isNull( properties.value( field.name() ) ) && static_cast<QMetaType::Type>( field.type() ) == QMetaType::QByteArray )
{
value = QByteArray::fromBase64( value.toByteArray() );
}
changedMap.insert( fieldIndex, value );
}
}
else
{
// Do nothing
}
fieldIndex++;
}
if ( ! changedMap.isEmpty() )
{
changedAttributes.insert( feature.id(), changedMap );
}
}
catch ( json::exception & )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Feature properties are not valid" ) );
}
}
catch ( json::exception & )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Feature properties are not valid" ) );
}
if ( changedAttributes.isEmpty() && changedGeometries.isEmpty() )
{
QgsMessageLog::logMessage( QStringLiteral( "Changeset is empty: no features have been modified" ), QStringLiteral( "Server" ), Qgis::MessageLevel::Info );
}
if ( ! mapLayer->dataProvider()->changeFeatures( changedAttributes, changedGeometries ) )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Error patching feature" ) );
}
// Now we need to send the updated feature to the client
feature = mapLayer->getFeature( feature.id() );
doGet();
break;
}
// //////////////////////////////////////////////////////////////
// Delete feature
case QgsServerRequest::Method::DeleteMethod:
{
// First: check permissions
const QStringList wfstDeleteLayerIds = QgsServerProjectUtils::wfstDeleteLayerIds( *context.project() );
if ( ! wfstDeleteLayerIds.contains( mapLayer->id() ) ||
! mapLayer->dataProvider()->capabilities().testFlag( Qgis::VectorProviderCapability::DeleteFeatures ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "Features in layer '%1' cannot be deleted" ).arg( mapLayer->name() ) );
}
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
if ( accessControl && !accessControl->layerDeletePermission( mapLayer ) )
{
throw QgsServerApiPermissionDeniedException( QStringLiteral( "No ACL permissions to delete features on layer '%1'" ).arg( mapLayer->name() ) );
}
//scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope
//there's LOTS of potential exit paths here, so we avoid having to restore the filters manually
std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() );
if ( accessControl )
{
QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, mapLayer, filterRestorer->originalFilters() );
}
#endif
if ( ! mapLayer->dataProvider()->deleteFeatures( { feature.id() } ) )
{
throw QgsServerApiInternalServerError( QStringLiteral( "Error deleting feature '%1' from layer '%2'" )
.arg( featureId )
.arg( mapLayer->name() ) );
}
// All good, empty response
json data = nullptr;
write( data, context );
break;
}
default:
{
throw QgsServerApiNotImplementedException( QStringLiteral( "%1 method is not implemented." )
.arg( QgsServerRequest::methodToString( context.request()->method() ) ) );
}
} // end switch
}
json QgsWfs3CollectionsFeatureHandler::schema( const QgsServerApiContext &context ) const
{
json data;
Q_ASSERT( context.project() );
const QVector<QgsVectorLayer *> layers { QgsServerApiUtils::publishedWfsLayers<QgsVectorLayer *>( context ) };
// Construct the context with collection id
for ( const auto &mapLayer : layers )
{
const QString shortName { mapLayer->serverProperties()->shortName().isEmpty() ? mapLayer->name() : mapLayer->serverProperties()->shortName() };
// Use layer id for operationId
const QString layerId { mapLayer->id() };
const std::string title { mapLayer->serverProperties()->wfsTitle().isEmpty() ? mapLayer->name().toStdString() : mapLayer->serverProperties()->wfsTitle().toStdString() };
const std::string path { QgsServerApiUtils::appendMapParameter( context.apiRootPath() + QStringLiteral( "/collections/%1/items/{featureId}" ).arg( shortName ), context.request()->url() ).toStdString() };
data[ path ] =
{
{
"get", {
{ "tags", jsonTags() },
{ "summary", "Retrieve a single feature from the '" + title + "' feature collection"},
{ "description", description() },
{ "operationId", operationId() + '_' + layerId.toStdString() + '_' + "GET" },
{
"parameters", {{ // array of objects
{ "$ref", "#/components/parameters/featureId" }
}
}
},
{
"responses", {
{
"200", {
{ "description", "Retrieve a '" + title + "' feature by 'featureId'." },
{
"content", {
{
"application/geo+json", {
{
"schema", {
{ "$ref", "#/components/schemas/featureGeoJSON" }
}
}
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
},
{ "default", defaultResponse() }
}
}
}
},
{
"put", {
{ "summary", "Replaces the feature with ID {featureId} in the collection {collectionId}" },
{ "tags", { "edit", "replace" } },
{ "description", "Replaces the feature with ID {featureId} in the collection {collectionId}" },
{ "operationId", operationId() + "PUT" },
{
"responses", {
{
"200", {
{ "description", "The feature was successfully updated" }
},
},
{
"403", {
{ "description", "Forbidden: the operation requested was not authorized" }
},
},
{
"500", {
{ "description", "Posted data could not be parsed correctly or another error occurred" }
},
},
{ "default", defaultResponse() }
}
}
}
},
{
"patch", {
{ "summary", "Changes attributes of feature with ID {featureId} in the collection {collectionId}" },
{ "tags", { "edit" } },
{ "description", "Changes attributes of feature with ID {featureId} in the collection {collectionId}" },
{ "operationId", operationId() + "PUT" },
{
"responses", {
{
"200", {
{ "description", "The feature was successfully updated" }
},
},
{
"403", {
{ "description", "Forbidden: the operation requested was not authorized" }
},
},
{
"500", {
{ "description", "Posted data could not be parsed correctly or another error occurred" }
},
},
{ "default", defaultResponse() }
}
}
}
},
{
"delete", {
{ "summary", "Deletes the feature with ID {featureId} in the collection {collectionId}" },
{ "tags", { "edit", "delete" } },
{ "description", "Deletes the feature with ID {featureId} in the collection {collectionId}" },
{ "operationId", operationId() + "DELETE" },
{
"responses", {
{
"201", {
{ "description", "The feature was successfully deleted from the collection" }
},
},
{
"403", {
{ "description", "Forbidden: the operation requested was not authorized" }
},
},
{
"500", {
{ "description", "Posted data could not be parsed correctly or another error occurred" }
}
},
{ "default", defaultResponse() }
}
}
}
}
};
#ifdef HAVE_SERVER_PYTHON_PLUGINS
// get access controls
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
// If the layer has no delete capabilities, remove the delete operation
if ( accessControl && !accessControl->layerDeletePermission( mapLayer ) )
{
data[ path ].erase( "delete" );
}
// If the layer has no update capabilities, remove the put and patch operation
if ( accessControl && !accessControl->layerUpdatePermission( mapLayer ) )
{
data[ path ].erase( "put" );
data[ path ].erase( "patch" );
}
#endif
} // end for loop
return data;
}