Skip to content

Commit f0f6157

Browse files
rouaultnyalldawson
authored andcommittedApr 3, 2023
[OAPIF provider] Part 2: only enable it if Part 2 conformance class is declared
1 parent 7413c35 commit f0f6157

File tree

7 files changed

+189
-3
lines changed

7 files changed

+189
-3
lines changed
 

‎src/providers/wfs/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(WFS_SRCS
2626
oapif/qgsoapiflandingpagerequest.cpp
2727
oapif/qgsoapifapirequest.cpp
2828
oapif/qgsoapifcollection.cpp
29+
oapif/qgsoapifconformancerequest.cpp
2930
oapif/qgsoapifitemsrequest.cpp
3031
oapif/qgsoapifprovider.cpp
3132
oapif/qgsoapifutils.cpp
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/***************************************************************************
2+
qgsoapifconformancerequest.cpp
3+
------------------------------
4+
begin : April 2023
5+
copyright : (C) 2023 by Even Rouault
6+
email : even.rouault at spatialys.com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include <nlohmann/json.hpp>
17+
using namespace nlohmann;
18+
19+
#include "qgslogger.h"
20+
#include "qgsoapifconformancerequest.h"
21+
#include "qgsoapifutils.h"
22+
#include "qgswfsconstants.h"
23+
24+
#include <QTextCodec>
25+
26+
QgsOapifConformanceRequest::QgsOapifConformanceRequest( const QgsDataSourceUri &uri ):
27+
QgsBaseNetworkRequest( QgsAuthorizationSettings( uri.username(), uri.password(), uri.authConfigId() ), "OAPIF" )
28+
{
29+
// Using Qt::DirectConnection since the download might be running on a different thread.
30+
// In this case, the request was sent from the main thread and is executed with the main
31+
// thread being blocked in future.waitForFinished() so we can run code on this object which
32+
// lives in the main thread without risking havoc.
33+
connect( this, &QgsBaseNetworkRequest::downloadFinished, this, &QgsOapifConformanceRequest::processReply, Qt::DirectConnection );
34+
}
35+
36+
QStringList QgsOapifConformanceRequest::conformanceClasses( const QUrl &conformanceUrl )
37+
{
38+
sendGET( conformanceUrl, QString( "application/json" ), /*synchronous=*/true, /*forceRefresh=*/false );
39+
return mConformanceClasses;
40+
}
41+
42+
QString QgsOapifConformanceRequest::errorMessageWithReason( const QString &reason )
43+
{
44+
return tr( "Download of conformance classes failed: %1" ).arg( reason );
45+
}
46+
47+
void QgsOapifConformanceRequest::processReply()
48+
{
49+
if ( mErrorCode != QgsBaseNetworkRequest::NoError )
50+
{
51+
return;
52+
}
53+
const QByteArray &buffer = mResponse;
54+
if ( buffer.isEmpty() )
55+
{
56+
mErrorMessage = tr( "empty response" );
57+
mErrorCode = QgsBaseNetworkRequest::ServerExceptionError;
58+
return;
59+
}
60+
61+
QgsDebugMsgLevel( QStringLiteral( "parsing Conformance response: " ) + buffer, 4 );
62+
63+
QTextCodec::ConverterState state;
64+
QTextCodec *codec = QTextCodec::codecForName( "UTF-8" );
65+
Q_ASSERT( codec );
66+
67+
const QString utf8Text = codec->toUnicode( buffer.constData(), buffer.size(), &state );
68+
if ( state.invalidChars != 0 )
69+
{
70+
mErrorCode = QgsBaseNetworkRequest::ApplicationLevelError;
71+
mErrorMessage = errorMessageWithReason( tr( "Invalid UTF-8 content" ) );
72+
return;
73+
}
74+
75+
try
76+
{
77+
const json j = json::parse( utf8Text.toStdString() );
78+
79+
if ( j.is_object() && j.contains( "conformsTo" ) )
80+
{
81+
const json jConformsTo = j["conformsTo"];
82+
if ( jConformsTo.is_array() )
83+
{
84+
for ( const auto &subj : jConformsTo )
85+
{
86+
if ( subj.is_string() )
87+
{
88+
mConformanceClasses.append( QString::fromStdString( subj.get<std::string>() ) );
89+
}
90+
}
91+
}
92+
}
93+
}
94+
catch ( const json::parse_error &ex )
95+
{
96+
mErrorCode = QgsBaseNetworkRequest::ApplicationLevelError;
97+
mErrorMessage = errorMessageWithReason( tr( "Cannot decode JSON document: %1" ).arg( QString::fromStdString( ex.what() ) ) );
98+
return;
99+
}
100+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/***************************************************************************
2+
qgsoapifconformancerequest.h
3+
-----------------------------
4+
begin : April 2023
5+
copyright : (C) 2023 by Even Rouault
6+
email : even.rouault at spatialys.com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSOAPIFCONFORMANCEREQUEST_H
17+
#define QGSOAPIFCONFORMANCEREQUEST_H
18+
19+
#include <QObject>
20+
21+
#include "qgsdatasourceuri.h"
22+
#include "qgsbasenetworkrequest.h"
23+
24+
//! Manages the conformance request
25+
class QgsOapifConformanceRequest : public QgsBaseNetworkRequest
26+
{
27+
Q_OBJECT
28+
public:
29+
explicit QgsOapifConformanceRequest( const QgsDataSourceUri &uri );
30+
31+
//! Issue the request synchronously and return conformance classes
32+
QStringList conformanceClasses( const QUrl &conformanceUrl );
33+
34+
private slots:
35+
void processReply();
36+
37+
private:
38+
QStringList mConformanceClasses;
39+
40+
protected:
41+
QString errorMessageWithReason( const QString &reason ) override;
42+
};
43+
44+
#endif // QGSOAPIFCONFORMANCEREQUEST_H

‎src/providers/wfs/oapif/qgsoapiflandingpagerequest.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ void QgsOapifLandingPageRequest::processReply()
124124
apiTypes );
125125
}
126126
#endif
127+
128+
mConformanceUrl = QgsOAPIFJson::findLink( links, QStringLiteral( "conformance" ) );
127129
}
128130
catch ( const json::parse_error &ex )
129131
{

‎src/providers/wfs/oapif/qgsoapiflandingpagerequest.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ class QgsOapifLandingPageRequest : public QgsBaseNetworkRequest
4545
//! Return URL of the api endpoint
4646
const QString &apiUrl() const { return mApiUrl; }
4747

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

51+
//! Return URL of the conformance endpoint
52+
const QString &conformanceUrl() const { return mConformanceUrl; }
53+
5154
signals:
5255
//! emitted when the capabilities have been fully parsed, or an error occurred
5356
void gotResponse();
@@ -67,6 +70,9 @@ class QgsOapifLandingPageRequest : public QgsBaseNetworkRequest
6770
//! URL of the collections endpoint.
6871
QString mCollectionsUrl;
6972

73+
//! URL of the conformance endpoint.
74+
QString mConformanceUrl;
75+
7076
ApplicationLevelError mAppLevelError = ApplicationLevelError::NoError;
7177

7278
};

‎src/providers/wfs/oapif/qgsoapifprovider.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "qgsoapiflandingpagerequest.h"
2121
#include "qgsoapifapirequest.h"
2222
#include "qgsoapifcollection.h"
23+
#include "qgsoapifconformancerequest.h"
2324
#include "qgsoapifitemsrequest.h"
2425
#include "qgswfsconstants.h"
2526
#include "qgswfsutils.h" // for isCompatibleType()
@@ -152,9 +153,26 @@ bool QgsOapifProvider::init()
152153
}
153154
}
154155

156+
bool implementsPart2 = false;
157+
const QString &conformanceUrl = landingPageRequest.conformanceUrl();
158+
if ( !conformanceUrl.isEmpty() )
159+
{
160+
QgsOapifConformanceRequest conformanceRequest( mShared->mURI.uri() );
161+
const QStringList conformanceClasses = conformanceRequest.conformanceClasses( conformanceUrl );
162+
implementsPart2 = conformanceClasses.contains( QLatin1String( "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs" ) );
163+
}
164+
155165
mLayerMetadata = collectionRequest->collection().mLayerMetadata;
156166

157-
if ( mLayerMetadata.crs().isValid() )
167+
QString srsName = mShared->mURI.SRSName();
168+
if ( implementsPart2 && !srsName.isEmpty() )
169+
{
170+
// Use URI SRSName parameter if defined
171+
mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( srsName );
172+
if ( mLayerMetadata.crs().isValid() && mShared->mSourceCrs.authid() == mLayerMetadata.crs().authid() )
173+
mShared->mSourceCrs.setCoordinateEpoch( mLayerMetadata.crs().coordinateEpoch() );
174+
}
175+
else if ( implementsPart2 && mLayerMetadata.crs().isValid() )
158176
{
159177
// WORKAROUND: Recreate a CRS object with fromOgcWmsCrs because when copying the
160178
// CRS his mPj pointer gets deleted and it is impossible to create a transform

‎tests/src/python/test_provider_oapif.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def GDAL_COMPUTE_VERSION(maj, min, rev):
6565
ACCEPT_LANDING = 'Accept=application/json'
6666
ACCEPT_API = 'Accept=application/vnd.oai.openapi+json;version=3.0, application/openapi+json;version=3.0, application/json'
6767
ACCEPT_COLLECTION = 'Accept=application/json'
68+
ACCEPT_CONFORMANCE = 'Accept=application/json'
6869
ACCEPT_ITEMS = 'Accept=application/geo+json, application/json'
6970

7071

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

9294
# API
@@ -104,6 +106,12 @@ def add_params(x, y):
104106
}
105107
}).encode('UTF-8'))
106108

109+
# conformance
110+
with open(sanitize(endpoint, '/conformance?' + add_params(extraparam, ACCEPT_CONFORMANCE)), 'wb') as f:
111+
f.write(json.dumps({
112+
"conformsTo": ["http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs"]
113+
}).encode('UTF-8'))
114+
107115
# collection
108116
collection = {
109117
"id": "mycollection",
@@ -860,6 +868,13 @@ def testCRS2056(self):
860868

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

871+
# Test srsname parameter overrides default CRS
872+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='mycollection' srsname='OGC:CRS84'", 'test', 'OAPIF')
873+
assert vl.isValid()
874+
source = vl.dataProvider()
875+
876+
self.assertEqual(source.sourceCrs().authid(), 'OGC:CRS84')
877+
863878
def testFeatureCountFallback(self):
864879

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

0 commit comments

Comments
 (0)
Please sign in to comment.