Skip to content

Commit

Permalink
[OAPIF provider] Part 2: only enable it if Part 2 conformance class i…
Browse files Browse the repository at this point in the history
…s declared
  • Loading branch information
rouault committed Apr 1, 2023
1 parent 0a6056a commit 50a5c8f
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/providers/wfs/CMakeLists.txt
Expand Up @@ -26,6 +26,7 @@ set(WFS_SRCS
oapif/qgsoapiflandingpagerequest.cpp
oapif/qgsoapifapirequest.cpp
oapif/qgsoapifcollection.cpp
oapif/qgsoapifconformancerequest.cpp
oapif/qgsoapifitemsrequest.cpp
oapif/qgsoapifprovider.cpp
oapif/qgsoapifutils.cpp
Expand Down
100 changes: 100 additions & 0 deletions src/providers/wfs/oapif/qgsoapifconformancerequest.cpp
@@ -0,0 +1,100 @@
/***************************************************************************
qgsoapifconformancerequest.cpp
------------------------------
begin : April 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 "qgsoapifconformancerequest.h"
#include "qgsoapifutils.h"
#include "qgswfsconstants.h"

#include <QTextCodec>

QgsOapifConformanceRequest::QgsOapifConformanceRequest( const QgsDataSourceUri &uri ):
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
{
// 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, &QgsOapifConformanceRequest::processReply, Qt::DirectConnection );
}

QStringList QgsOapifConformanceRequest::conformanceClasses( const QUrl &conformanceUrl )
{
sendGET( conformanceUrl, QString( "application/json" ), /*synchronous=*/true, /*forceRefresh=*/false );
return mConformanceClasses;
}

QString QgsOapifConformanceRequest::errorMessageWithReason( const QString &reason )
{
return tr( "Download of conformance classes failed: %1" ).arg( reason );
}

void QgsOapifConformanceRequest::processReply()
{
if ( mErrorCode != QgsBaseNetworkRequest::NoError )
{
return;
}
const QByteArray &buffer = mResponse;
if ( buffer.isEmpty() )
{
mErrorMessage = tr( "empty response" );
mErrorCode = QgsBaseNetworkRequest::ServerExceptionError;
return;
}

QgsDebugMsgLevel( QStringLiteral( "parsing Conformance response: " ) + buffer, 4 );

QTextCodec::ConverterState state;
QTextCodec *codec = QTextCodec::codecForName( "UTF-8" );
Q_ASSERT( codec );

const QString utf8Text = codec->toUnicode( buffer.constData(), buffer.size(), &state );
if ( state.invalidChars != 0 )
{
mErrorCode = QgsBaseNetworkRequest::ApplicationLevelError;
mErrorMessage = errorMessageWithReason( tr( "Invalid UTF-8 content" ) );
return;
}

try
{
const json j = json::parse( utf8Text.toStdString() );

if ( j.is_object() && j.contains( "conformsTo" ) )
{
const json jConformsTo = j["conformsTo"];
if ( jConformsTo.is_array() )
{
for ( const auto &subj : jConformsTo )
{
if ( subj.is_string() )
{
mConformanceClasses.append( QString::fromStdString( subj.get<std::string>() ) );
}
}
}
}
}
catch ( const json::parse_error &ex )
{
mErrorCode = QgsBaseNetworkRequest::ApplicationLevelError;
mErrorMessage = errorMessageWithReason( tr( "Cannot decode JSON document: %1" ).arg( QString::fromStdString( ex.what() ) ) );
return;
}
}
44 changes: 44 additions & 0 deletions src/providers/wfs/oapif/qgsoapifconformancerequest.h
@@ -0,0 +1,44 @@
/***************************************************************************
qgsoapifconformancerequest.h
-----------------------------
begin : April 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 QGSOAPIFCONFORMANCEREQUEST_H
#define QGSOAPIFCONFORMANCEREQUEST_H

#include <QObject>

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

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

//! Issue the request synchronously and return conformance classes
QStringList conformanceClasses( const QUrl &conformanceUrl );

private slots:
void processReply();

private:
QStringList mConformanceClasses;

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

#endif // QGSOAPIFCONFORMANCEREQUEST_H
2 changes: 2 additions & 0 deletions src/providers/wfs/oapif/qgsoapiflandingpagerequest.cpp
Expand Up @@ -124,6 +124,8 @@ void QgsOapifLandingPageRequest::processReply()
apiTypes );
}
#endif

mConformanceUrl = QgsOAPIFJson::findLink( links, QStringLiteral( "conformance" ) );
}
catch ( const json::parse_error &ex )
{
Expand Down
8 changes: 7 additions & 1 deletion src/providers/wfs/oapif/qgsoapiflandingpagerequest.h
Expand Up @@ -45,9 +45,12 @@ class QgsOapifLandingPageRequest : public QgsBaseNetworkRequest
//! Return URL of the api endpoint
const QString &apiUrl() const { return mApiUrl; }

//! Return URL of the api endpoint
//! Return URL of the collections endpoint
const QString &collectionsUrl() const { return mCollectionsUrl; }

//! Return URL of the conformance endpoint
const QString &conformanceUrl() const { return mConformanceUrl; }

signals:
//! emitted when the capabilities have been fully parsed, or an error occurred
void gotResponse();
Expand All @@ -67,6 +70,9 @@ class QgsOapifLandingPageRequest : public QgsBaseNetworkRequest
//! URL of the collections endpoint.
QString mCollectionsUrl;

//! URL of the conformance endpoint.
QString mConformanceUrl;

ApplicationLevelError mAppLevelError = ApplicationLevelError::NoError;

};
Expand Down
20 changes: 19 additions & 1 deletion src/providers/wfs/oapif/qgsoapifprovider.cpp
Expand Up @@ -20,6 +20,7 @@
#include "qgsoapiflandingpagerequest.h"
#include "qgsoapifapirequest.h"
#include "qgsoapifcollection.h"
#include "qgsoapifconformancerequest.h"
#include "qgsoapifitemsrequest.h"
#include "qgswfsconstants.h"
#include "qgswfsutils.h" // for isCompatibleType()
Expand Down Expand Up @@ -152,9 +153,26 @@ bool QgsOapifProvider::init()
}
}

bool implementsPart2 = false;
const QString &conformanceUrl = landingPageRequest.conformanceUrl();
if ( !conformanceUrl.isEmpty() )
{
QgsOapifConformanceRequest conformanceRequest( mShared->mURI.uri() );
const QStringList conformanceClasses = conformanceRequest.conformanceClasses( conformanceUrl );
implementsPart2 = conformanceClasses.contains( QLatin1String( "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs" ) );
}

mLayerMetadata = collectionRequest->collection().mLayerMetadata;

if ( mLayerMetadata.crs().isValid() )
QString srsName = mShared->mURI.SRSName();
if ( implementsPart2 && !srsName.isEmpty() )
{
// Use URI SRSName parameter if defined
mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( srsName );
if ( mLayerMetadata.crs().isValid() && mShared->mSourceCrs.authid() == mLayerMetadata.crs().authid() )
mShared->mSourceCrs.setCoordinateEpoch( mLayerMetadata.crs().coordinateEpoch() );
}
else if ( implementsPart2 && mLayerMetadata.crs().isValid() )
{
// WORKAROUND: Recreate a CRS object with fromOgcWmsCrs because when copying the
// CRS his mPj pointer gets deleted and it is impossible to create a transform
Expand Down
17 changes: 16 additions & 1 deletion tests/src/python/test_provider_oapif.py
Expand Up @@ -65,6 +65,7 @@ def GDAL_COMPUTE_VERSION(maj, min, rev):
ACCEPT_LANDING = 'Accept=application/json'
ACCEPT_API = 'Accept=application/vnd.oai.openapi+json;version=3.0, application/openapi+json;version=3.0, application/json'
ACCEPT_COLLECTION = 'Accept=application/json'
ACCEPT_CONFORMANCE = 'Accept=application/json'
ACCEPT_ITEMS = 'Accept=application/geo+json, application/json'


Expand All @@ -86,7 +87,8 @@ def add_params(x, y):
f.write(json.dumps({
"links": [
{"href": "http://" + endpoint + "/api" + questionmark_extraparam, "rel": "service-desc"},
{"href": "http://" + endpoint + "/collections" + questionmark_extraparam, "rel": "data"}
{"href": "http://" + endpoint + "/collections" + questionmark_extraparam, "rel": "data"},
{"href": "http://" + endpoint + "/conformance" + questionmark_extraparam, "rel": "conformance"},
]}).encode('UTF-8'))

# API
Expand All @@ -104,6 +106,12 @@ def add_params(x, y):
}
}).encode('UTF-8'))

# conformance
with open(sanitize(endpoint, '/conformance?' + add_params(extraparam, ACCEPT_CONFORMANCE)), 'wb') as f:
f.write(json.dumps({
"conformsTo": ["http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs"]
}).encode('UTF-8'))

# collection
collection = {
"id": "mycollection",
Expand Down Expand Up @@ -860,6 +868,13 @@ def testCRS2056(self):

self.assertEqual(source.sourceCrs().authid(), 'EPSG:2056')

# Test srsname parameter overrides default CRS
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='mycollection' srsname='OGC:CRS84'", 'test', 'OAPIF')
assert vl.isValid()
source = vl.dataProvider()

self.assertEqual(source.sourceCrs().authid(), 'OGC:CRS84')

def testFeatureCountFallback(self):

# On Windows we must make sure that any backslash in the path is
Expand Down

0 comments on commit 50a5c8f

Please sign in to comment.