Skip to content

Commit a19efb1

Browse files
rouaultnyalldawson
authored andcommittedApr 24, 2023
[OAPIF provider] Implements getFeature() expression translation
1 parent e10bd31 commit a19efb1

File tree

3 files changed

+165
-37
lines changed

3 files changed

+165
-37
lines changed
 

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

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -872,10 +872,10 @@ static void collectTopLevelAndNodes( const QgsExpressionNode *node,
872872
topAndNodes.push_back( node );
873873
}
874874

875-
QString QgsOapifSharedData::translateNodeToServer(
875+
QString QgsOapifSharedData::compileExpressionNodeUsingPart1(
876876
const QgsExpressionNode *rootNode,
877877
QgsOapifProvider::FilterTranslationState &translationState,
878-
QString &untranslatedPart )
878+
QString &untranslatedPart ) const
879879
{
880880
std::vector<const QgsExpressionNode *> topAndNodes;
881881
collectTopLevelAndNodes( rootNode, topAndNodes );
@@ -1035,18 +1035,15 @@ QString QgsOapifSharedData::translateNodeToServer(
10351035
return ret;
10361036
}
10371037

1038-
bool QgsOapifSharedData::computeServerFilter( QString &errorMsg )
1038+
bool QgsOapifSharedData::computeFilter( const QgsExpression &expr,
1039+
QgsOapifProvider::FilterTranslationState &translationState,
1040+
QString &serverSideParameters,
1041+
QString &clientSideFilterExpression ) const
10391042
{
1040-
errorMsg.clear();
1041-
mClientSideFilterExpression = mURI.filter();
1042-
mServerFilter.clear();
1043-
if ( mClientSideFilterExpression.isEmpty() )
1044-
{
1045-
mFilterTranslationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
1046-
return true;
1047-
}
1043+
const auto rootNode = expr.rootNode();
1044+
if ( !rootNode )
1045+
return false;
10481046

1049-
const QgsExpression expr( mClientSideFilterExpression );
10501047
if ( mServerSupportsFilterCql2Text )
10511048
{
10521049
const bool invertAxisOrientation = mSourceCrs.hasAxisInverted();
@@ -1056,54 +1053,73 @@ bool QgsOapifSharedData::computeServerFilter( QString &errorMsg )
10561053
QgsOapifCql2TextExpressionCompiler::Result res = compiler.compile( &expr );
10571054
if ( res == QgsOapifCql2TextExpressionCompiler::Fail )
10581055
{
1059-
QgsDebugMsg( "Whole filter will be evaluated on client-side" );
1060-
mFilterTranslationState = QgsOapifProvider::FilterTranslationState::FULLY_CLIENT;
1056+
clientSideFilterExpression = expr.rootNode()->dump();
1057+
translationState = QgsOapifProvider::FilterTranslationState::FULLY_CLIENT;
10611058
return true;
10621059
}
1063-
mServerFilter = getEncodedQueryParam( QStringLiteral( "filter" ), compiler.result() );
1064-
mServerFilter += QStringLiteral( "&filter-lang=cql2-text" );
1060+
serverSideParameters = getEncodedQueryParam( QStringLiteral( "filter" ), compiler.result() );
1061+
serverSideParameters += QStringLiteral( "&filter-lang=cql2-text" );
10651062
if ( compiler.geometryLiteralUsed() )
10661063
{
10671064
if ( mSourceCrs
10681065
!= QgsCoordinateReferenceSystem::fromOgcWmsCrs( QgsOapifProvider::OAPIF_PROVIDER_DEFAULT_CRS ) )
10691066
{
1070-
mServerFilter += QStringLiteral( "&filter-crs=%1" ).arg( mSourceCrs.toOgcUri() );
1067+
serverSideParameters += QStringLiteral( "&filter-crs=%1" ).arg( mSourceCrs.toOgcUri() );
10711068
}
10721069
}
10731070

1071+
clientSideFilterExpression.clear();
10741072
if ( res == QgsOapifCql2TextExpressionCompiler::Partial )
10751073
{
1076-
mFilterTranslationState = QgsOapifProvider::FilterTranslationState::PARTIAL;
1077-
QgsDebugMsg( "Part of the filter will be evaluated on client-side" );
1074+
translationState = QgsOapifProvider::FilterTranslationState::PARTIAL;
10781075
}
10791076
else
10801077
{
1081-
mFilterTranslationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
1078+
translationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
10821079
}
10831080
return true;
10841081
}
10851082

1086-
const auto rootNode = expr.rootNode();
1087-
if ( !rootNode )
1088-
return false;
1083+
serverSideParameters = compileExpressionNodeUsingPart1( rootNode, translationState, clientSideFilterExpression );
1084+
return true;
1085+
}
10891086

1090-
mServerFilter = translateNodeToServer( rootNode, mFilterTranslationState, mClientSideFilterExpression );
1091-
if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::PARTIAL )
1087+
bool QgsOapifSharedData::computeServerFilter( QString &errorMsg )
1088+
{
1089+
errorMsg.clear();
1090+
mClientSideFilterExpression = mURI.filter();
1091+
mServerFilter.clear();
1092+
if ( mClientSideFilterExpression.isEmpty() )
10921093
{
1093-
QgsDebugMsg( QStringLiteral( "Part of the filter will be evaluated on client-side: %1" ).arg( mClientSideFilterExpression ) );
1094+
mFilterTranslationState = QgsOapifProvider::FilterTranslationState::FULLY_SERVER;
1095+
return true;
10941096
}
1095-
else if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::FULLY_CLIENT )
1097+
1098+
const QgsExpression expr( mClientSideFilterExpression );
1099+
bool ret = computeFilter( expr, mFilterTranslationState, mServerFilter, mClientSideFilterExpression );
1100+
if ( ret )
10961101
{
1097-
QgsDebugMsg( "Whole filter will be evaluated on client-side" );
1102+
if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::PARTIAL )
1103+
{
1104+
QgsDebugMsg( QStringLiteral( "Part of the filter will be evaluated on client-side: %1" ).arg( mClientSideFilterExpression ) );
1105+
}
1106+
else if ( mFilterTranslationState == QgsOapifProvider::FilterTranslationState::FULLY_CLIENT )
1107+
{
1108+
QgsDebugMsg( "Whole filter will be evaluated on client-side" );
1109+
}
10981110
}
1099-
1100-
return true;
1111+
return ret;
11011112
}
11021113

11031114
QString QgsOapifSharedData::computedExpression( const QgsExpression &expression ) const
11041115
{
1105-
Q_UNUSED( expression );
1106-
return QString();
1116+
if ( !expression.isValid() )
1117+
return QString();
1118+
QgsOapifProvider::FilterTranslationState translationState;
1119+
QString serverParameters;
1120+
QString clientSideFilterExpression;
1121+
computeFilter( expression, translationState, serverParameters, clientSideFilterExpression );
1122+
return serverParameters;
11071123
}
11081124

11091125
void QgsOapifSharedData::pushError( const QString &errorMsg ) const
@@ -1178,10 +1194,50 @@ void QgsOapifFeatureDownloaderImpl::run( bool serializeFeatures, long long maxFe
11781194
hasQueryParam = true;
11791195
}
11801196

1197+
// mServerFilter comes from the translation of the uri "filter" parameter
1198+
// mServerExpression comes from the translation of a getFeatures() expression
11811199
if ( !mShared->mServerFilter.isEmpty() )
11821200
{
11831201
url += ( hasQueryParam ? QStringLiteral( "&" ) : QStringLiteral( "?" ) );
1184-
url += mShared->mServerFilter;
1202+
if ( !mShared->mServerExpression.isEmpty() )
1203+
{
1204+
// Combine mServerFilter and mServerExpression
1205+
QStringList components1 = mShared->mServerFilter.split( QLatin1Char( '&' ) );
1206+
QStringList components2 = mShared->mServerExpression.split( QLatin1Char( '&' ) );
1207+
Q_ASSERT( components1[0].startsWith( QStringLiteral( "filter=" ) ) );
1208+
Q_ASSERT( components2[0].startsWith( QStringLiteral( "filter=" ) ) );
1209+
url += QStringLiteral( "filter=" );
1210+
url += '(';
1211+
url += components1[0].mid( static_cast<int>( strlen( "filter=" ) ) );
1212+
url += QStringLiteral( ") AND (" );
1213+
url += components2[0].mid( static_cast<int>( strlen( "filter=" ) ) );
1214+
url += ')';
1215+
// Add components1 extra parameters: filter-lang and filter-crs
1216+
for ( int i = 1; i < components1.size(); ++i )
1217+
{
1218+
url += '&';
1219+
url += components1[i];
1220+
}
1221+
// Add components2 extra parameters, not already included in components1
1222+
for ( int i = 1; i < components2.size(); ++i )
1223+
{
1224+
if ( !components1.contains( components2[i] ) )
1225+
{
1226+
url += '&';
1227+
url += components1[i];
1228+
}
1229+
}
1230+
}
1231+
else
1232+
{
1233+
url += mShared->mServerFilter;
1234+
}
1235+
hasQueryParam = true;
1236+
}
1237+
else if ( !mShared->mServerExpression.isEmpty() )
1238+
{
1239+
url += ( hasQueryParam ? QStringLiteral( "&" ) : QStringLiteral( "?" ) );
1240+
url += mShared->mServerExpression;
11851241
hasQueryParam = true;
11861242
}
11871243

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,16 @@ class QgsOapifSharedData final: public QObject, public QgsBackgroundCachedShared
240240

241241
private:
242242

243-
// Translate part of an expression to a server-side filter
244-
QString translateNodeToServer( const QgsExpressionNode *node,
245-
QgsOapifProvider::FilterTranslationState &translationState,
246-
QString &untranslatedPart );
243+
// Translate part of an expression to a server-side filter using Part1 features only
244+
QString compileExpressionNodeUsingPart1( const QgsExpressionNode *node,
245+
QgsOapifProvider::FilterTranslationState &translationState,
246+
QString &untranslatedPart ) const;
247+
248+
// Translate part of an expression to a server-side filter using Part1 or Part3
249+
bool computeFilter( const QgsExpression &expr,
250+
QgsOapifProvider::FilterTranslationState &translationState,
251+
QString &serverSideParameters,
252+
QString &clientSideFilterExpression ) const;
247253

248254
//! Log error to QgsMessageLog and raise it to the provider
249255
void pushError( const QString &errorMsg ) const override;

‎tests/src/python/test_provider_oapif.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,72 @@ def testCQL2TextFilteringAndPart2(self):
10381038
os.unlink(filename)
10391039
self.assertEqual(values, ['feat.1'], expr)
10401040

1041+
def testCQL2TextFilteringGetFeaturesExpression(self):
1042+
"""Test Part 3 CQL2-Text filtering"""
1043+
1044+
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_encoded_query_testCQL2TextFilteringGetFeaturesExpression'
1045+
additionalConformance = [
1046+
"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
1047+
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
1048+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter",
1049+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter",
1050+
]
1051+
1052+
create_landing_page_api_collection(endpoint, additionalConformance=additionalConformance)
1053+
1054+
filename = sanitize(endpoint, '/collections/mycollection/queryables?' + ACCEPT_QUERYABLES)
1055+
queryables = {
1056+
"properties": {
1057+
"strfield": {
1058+
"type": "string"
1059+
},
1060+
"geometry": {
1061+
"$ref": "https://geojson.org/schema/Point.json"
1062+
},
1063+
}
1064+
}
1065+
with open(filename, 'wb') as f:
1066+
f.write(json.dumps(queryables).encode('UTF-8'))
1067+
1068+
items = {
1069+
"type": "FeatureCollection",
1070+
"features": [
1071+
{"type": "Feature", "id": "feat.1", "properties":
1072+
{
1073+
"strfield": "foo=bar",
1074+
},
1075+
"geometry": {"type": "Point", "coordinates": [-70.5, 66.5]}}
1076+
]
1077+
}
1078+
1079+
filename = sanitize(endpoint, '/collections/mycollection/items?limit=10&' + ACCEPT_ITEMS)
1080+
with open(filename, 'wb') as f:
1081+
f.write(json.dumps(items).encode('UTF-8'))
1082+
1083+
vl = QgsVectorLayer(
1084+
"url='http://" + endpoint + "' typename='mycollection'", 'test', 'OAPIF')
1085+
self.assertTrue(vl.isValid())
1086+
os.unlink(filename)
1087+
1088+
tests = [
1089+
("", """"strfield" = 'foo=bar'""",
1090+
"""filter=(strfield%20%3D%20'foo%3Dbar')&filter-lang=cql2-text"""),
1091+
("strfield <> 'x'", """"strfield" = 'foo=bar'""",
1092+
"""filter=((strfield%20%3C%3E%20'x'))%20AND%20((strfield%20%3D%20'foo%3Dbar'))&filter-lang=cql2-text"""),
1093+
]
1094+
for (substring_expr, getfeatures_expr, cql_filter) in tests:
1095+
assert vl.setSubsetString(substring_expr)
1096+
filename = sanitize(endpoint,
1097+
"/collections/mycollection/items?limit=1000&" + cql_filter + ("&" if cql_filter else "") + ACCEPT_ITEMS)
1098+
with open(filename, 'wb') as f:
1099+
f.write(json.dumps(items).encode('UTF-8'))
1100+
request = QgsFeatureRequest()
1101+
if getfeatures_expr:
1102+
request.setFilterExpression(getfeatures_expr)
1103+
values = [f['id'] for f in vl.getFeatures(request)]
1104+
os.unlink(filename)
1105+
self.assertEqual(values, ['feat.1'], (substring_expr, getfeatures_expr))
1106+
10411107
def testStringList(self):
10421108

10431109
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_testStringList'

0 commit comments

Comments
 (0)
Please sign in to comment.