Skip to content

Commit

Permalink
WFS: set namespace prefix on geometry and attribute elements in FILTE…
Browse files Browse the repository at this point in the history
…R when the typename has an explicit namespace (fixes #43957)
  • Loading branch information
rouault committed Sep 14, 2021
1 parent ce7bc16 commit dadc4d5
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 3 deletions.
22 changes: 21 additions & 1 deletion src/core/qgsogcutils.cpp
Expand Up @@ -1984,6 +1984,20 @@ QDomElement QgsOgcUtils::SQLStatementToOgcFilter( const QgsSQLStatement &stateme
attr.setValue( GML_NAMESPACE );
filterElem.setAttributeNode( attr );
}

QSet<QString> setNamespaceURI;
for ( const LayerProperties &props : layerProperties )
{
if ( !props.mNamespacePrefix.isEmpty() && !props.mNamespaceURI.isEmpty() &&
!setNamespaceURI.contains( props.mNamespaceURI ) )
{
setNamespaceURI.insert( props.mNamespaceURI );
QDomAttr attr = doc.createAttribute( QStringLiteral( "xmlns:" ) + props.mNamespacePrefix );
attr.setValue( props.mNamespaceURI );
filterElem.setAttributeNode( attr );
}
}

filterElem.appendChild( exprRootElem );
return filterElem;
}
Expand Down Expand Up @@ -2611,7 +2625,13 @@ QDomElement QgsOgcUtilsSQLStatementToFilter::toOgcFilter( const QgsSQLStatement:
{
QDomElement propElem = mDoc.createElement( mFilterPrefix + ":" + mPropertyName );
if ( node->tableName().isEmpty() || mLayerProperties.size() == 1 )
propElem.appendChild( mDoc.createTextNode( node->name() ) );
{
if ( mLayerProperties.size() == 1 && !mLayerProperties[0].mNamespacePrefix.isEmpty() )
propElem.appendChild( mDoc.createTextNode(
mLayerProperties[0].mNamespacePrefix + QStringLiteral( ":" ) + node->name() ) );
else
propElem.appendChild( mDoc.createTextNode( node->name() ) );
}
else
{
QString tableName( mMapTableAliasToNames[node->tableName()] );
Expand Down
4 changes: 4 additions & 0 deletions src/core/qgsogcutils.h
Expand Up @@ -250,6 +250,10 @@ class CORE_EXPORT QgsOgcUtils
QString mGeometryAttribute;
//! SRS name
QString mSRSName;
//! Namespace prefix
QString mNamespacePrefix;
//! Namespace URI
QString mNamespaceURI;
};
#endif

Expand Down
2 changes: 1 addition & 1 deletion src/providers/wfs/qgswfscapabilities.cpp
Expand Up @@ -109,7 +109,7 @@ QString QgsWfsCapabilities::Capabilities::getNamespaceParameterValue( const QStr
bool tryNameSpacing = ( !namespaces.isEmpty() && typeName.contains( ':' ) );
if ( tryNameSpacing )
{
QString prefixOfTypename = typeName.section( ':', 0, 0 );
QString prefixOfTypename = QgsWFSUtils::nameSpacePrefix( typeName );
return "xmlns(" + prefixOfTypename +
( WFSVersion.startsWith( QLatin1String( "2.0" ) ) ? "," : "=" ) +
namespaces + ")";
Expand Down
26 changes: 25 additions & 1 deletion src/providers/wfs/qgswfsfeatureiterator.cpp
Expand Up @@ -121,11 +121,20 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, int maxFeatures,
}
else
{
Q_FOREACH ( const QgsOgcUtils::LayerProperties layerProperties, mShared->mLayerPropertiesList )
QSet<QString> setNamespaces;
for ( const QgsOgcUtils::LayerProperties &layerProperties : qgis::as_const( mShared->mLayerPropertiesList ) )
{
if ( !typenames.isEmpty() )
typenames += QLatin1Char( ',' );
typenames += layerProperties.mName;
const QString lNamespace = mShared->mCaps.getNamespaceParameterValue( mShared->mWFSVersion, layerProperties.mName );
if ( !lNamespace.isEmpty() && !setNamespaces.contains( lNamespace ) )
{
if ( !namespaces.isEmpty() )
namespaces += QLatin1Char( ',' );
namespaces += lNamespace;
setNamespaces.insert( lNamespace );
}
}
}
if ( mShared->mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
Expand Down Expand Up @@ -196,6 +205,8 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, int maxFeatures,
QString geometryAttribute( mShared->mGeometryAttribute );
if ( mShared->mLayerPropertiesList.size() > 1 )
geometryAttribute = mShared->mURI.typeName() + "/" + geometryAttribute;
else if ( mShared->mLayerPropertiesList.size() == 1 && !mShared->mLayerPropertiesList[0].mNamespacePrefix.isEmpty() )
geometryAttribute = mShared->mLayerPropertiesList[0].mNamespacePrefix + QStringLiteral( ":" ) + geometryAttribute;
QDomElement bboxElem = QgsOgcUtils::expressionToOgcFilter( bboxExp, doc,
gmlVersion, filterVersion, geometryAttribute, mShared->srsName(),
honourAxisOrientation, mShared->mURI.invertAxisOrientation() );
Expand All @@ -213,6 +224,19 @@ QUrl QgsWFSFeatureDownloaderImpl::buildURL( qint64 startIndex, int maxFeatures,
andElem.appendChild( filterNode );
doc.firstChildElement().appendChild( andElem );

QSet<QString> setNamespaceURI;
for ( const QgsOgcUtils::LayerProperties &props : qgis::as_const( mShared->mLayerPropertiesList ) )
{
if ( !props.mNamespacePrefix.isEmpty() && !props.mNamespaceURI.isEmpty() &&
!setNamespaceURI.contains( props.mNamespaceURI ) )
{
setNamespaceURI.insert( props.mNamespaceURI );
QDomAttr attr = doc.createAttribute( QStringLiteral( "xmlns:" ) + props.mNamespacePrefix );
attr.setValue( props.mNamespaceURI );
doc.firstChildElement().setAttributeNode( attr );
}
}

query.addQueryItem( QStringLiteral( "FILTER" ), sanitizeFilter( doc.toString() ) );
}
else if ( !rect.isNull() )
Expand Down
6 changes: 6 additions & 0 deletions src/providers/wfs/qgswfsprovider.cpp
Expand Up @@ -487,6 +487,12 @@ bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QS
if ( typeName == mShared->mURI.typeName() )
layerProperties.mSRSName = mShared->srsName();

if ( typeName.contains( ':' ) )
{
layerProperties.mNamespaceURI = mShared->mCaps.getNamespaceForTypename( typeName );
layerProperties.mNamespacePrefix = QgsWFSUtils::nameSpacePrefix( typeName );
}

mShared->mLayerPropertiesList << layerProperties;
}

Expand Down
17 changes: 17 additions & 0 deletions tests/src/core/testqgsogcutils.cpp
Expand Up @@ -1192,6 +1192,23 @@ void TestQgsOgcUtils::testSQLStatementToOgcFilter_data()
"</fes:PropertyIsEqualTo>"
"</fes:And>"
"</fes:Filter>" );

QList<QgsOgcUtils::LayerProperties> layerPropertiesWithNameSpace;
QgsOgcUtils::LayerProperties props;
props.mName = QStringLiteral( "prefix:mylayer" );
props.mNamespacePrefix = QStringLiteral( "prefix" );
props.mNamespaceURI = QStringLiteral( "http://example.com/prefix" );
layerPropertiesWithNameSpace << props;

QTest::newRow( "namespace" ) << QStringLiteral( "SELECT * FROM mylayer WHERE NAME = 'New York'" ) <<
QgsOgcUtils::GML_3_2_1 << QgsOgcUtils::FILTER_FES_2_0 << layerPropertiesWithNameSpace <<
QString(
"<fes:Filter xmlns:fes=\"http://www.opengis.net/fes/2.0\" xmlns:prefix=\"http://example.com/prefix\">"
"<fes:PropertyIsEqualTo>"
"<fes:ValueReference>prefix:NAME</fes:ValueReference>"
"<fes:Literal>New York</fes:Literal>"
"</fes:PropertyIsEqualTo>"
"</fes:Filter>" );
}

QGSTEST_MAIN( TestQgsOgcUtils )
Expand Down
113 changes: 113 additions & 0 deletions tests/src/python/test_provider_wfs.py
Expand Up @@ -3301,6 +3301,119 @@ def testGetFeatureWithNamespaces(self):
values = [f['intfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1])

def testGetFeatureWithNamespaceAndFilter(self):
''' test https://github.com/qgis/QGIS/issues/43957 '''

endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_getfeature_with_namespace_and_filter'

with open(sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=2.0.0'), 'wb') as f:
f.write("""
<wfs:WFS_Capabilities version="2.0.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:ows="http://www.opengis.net/ows/1.1">
<wfs:FeatureTypeList>
<wfs:FeatureType xmlns:my="http://my">
<wfs:Name>my:typename</wfs:Name>
<wfs:Title>Title</wfs:Title>
<wfs:Abstract>Abstract</wfs:Abstract>
<wfs:SRS>EPSG:32631</wfs:SRS>
<WGS84BoundingBox>
<LowerCorner>0 40</LowerCorner>
<UpperCorner>15 50</UpperCorner>
</WGS84BoundingBox>
</wfs:FeatureType>
</wfs:FeatureTypeList>
</wfs:WFS_Capabilities>""".encode('UTF-8'))

with open(sanitize(endpoint,
'?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAMES=my:typename&NAMESPACES=xmlns(my,http://my)&TYPENAME=my:typename&NAMESPACE=xmlns(my,http://my)'),
'wb') as f:
f.write("""
<xsd:schema xmlns:my="http://my" xmlns:gml="http://www.opengis.net/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://my">
<xsd:import namespace="http://www.opengis.net/gml"/>
<xsd:complexType name="typenameType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometryProperty" nillable="true" type="gml:PointPropertyType"/>
<xsd:element maxOccurs="1" minOccurs="0" name="intfield" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
</xsd:schema>
""".encode('UTF-8'))

# SQL query with type with namespace
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='2.0.0' sql=SELECT * FROM \"my:typename\" WHERE intfield = 1", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.fields()), 1)

with open(sanitize(endpoint,
"""?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::32631&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:my="http://my">
<fes:PropertyIsEqualTo>
<fes:ValueReference>my:intfield</fes:ValueReference>
<fes:Literal>1</fes:Literal>
</fes:PropertyIsEqualTo>
</fes:Filter>
&NAMESPACES=xmlns(my,http://my)&NAMESPACE=xmlns(my,http://my)"""),
'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<gml:featureMember>
<my:typename fid="typename.0">
<my:intfield>1</my:intfield>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))

values = [f['intfield'] for f in vl.getFeatures()]
self.assertEqual(values, [1])

# SQL query with type with namespace and bounding box
vl = QgsVectorLayer(
"url='http://" + endpoint + "' typename='my:typename' version='2.0.0' restrictToRequestBBOX=1 sql=SELECT * FROM \"my:typename\" WHERE intfield > 0", 'test',
'WFS')

extent = QgsRectangle(400000.0, 5400000.0, 450000.0, 5500000.0)
request = QgsFeatureRequest().setFilterRect(extent)

with open(sanitize(endpoint,
"""?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my:typename&SRSNAME=urn:ogc:def:crs:EPSG::32631&FILTER=<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:my="http://my">
<fes:And>
<fes:BBOX>
<fes:ValueReference>my:geometryProperty</fes:ValueReference>
<gml:Envelope srsName="urn:ogc:def:crs:EPSG::32631">
<gml:lowerCorner>400000 5400000</gml:lowerCorner>
<gml:upperCorner>450000 5500000</gml:upperCorner>
</gml:Envelope>
</fes:BBOX>
<fes:PropertyIsGreaterThan xmlns:fes="http://www.opengis.net/fes/2.0">
<fes:ValueReference>my:intfield</fes:ValueReference>
<fes:Literal xmlns:fes="http://www.opengis.net/fes/2.0">0</fes:Literal>
</fes:PropertyIsGreaterThan>
</fes:And>
</fes:Filter>
&NAMESPACES=xmlns(my,http://my)&NAMESPACE=xmlns(my,http://my)"""),
'wb') as f:
f.write("""
<wfs:FeatureCollection
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:my="http://my">
<gml:featureMember>
<my:typename fid="typename.0">
<my:geometryProperty><gml:Point srsName="urn:ogc:def:crs:EPSG::32631" gml:id="typename.geom.0"><gml:pos>426858 5427937</gml:pos></gml:Point></my:geometryProperty>
<my:intfield>1</my:intfield>
</my:typename>
</gml:featureMember>
</wfs:FeatureCollection>""".encode('UTF-8'))

values = [f['intfield'] for f in vl.getFeatures(request)]
self.assertEqual(values, [1])

def testExtentSubsetString(self):
# can't run the base provider test suite here - WFS/OAPIF extent handling is different
# to other providers
Expand Down

0 comments on commit dadc4d5

Please sign in to comment.