Skip to content

Commit 5eb5139

Browse files
committedMay 25, 2019
[WFS provider] Support layers with GML field names only differing by cases (github fixes #29858)
1 parent 3f46a55 commit 5eb5139

File tree

5 files changed

+127
-10
lines changed

5 files changed

+127
-10
lines changed
 

‎src/providers/wfs/qgswfsfeatureiterator.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ QgsFeatureRequest QgsWFSFeatureIterator::buildRequestCache( int genCounter )
10341034
const auto subsetOfAttributes = mRequest.subsetOfAttributes();
10351035
for ( int i : subsetOfAttributes )
10361036
{
1037-
int idx = dataProviderFields.indexFromName( mShared->mFields.at( i ).name() );
1037+
int idx = dataProviderFields.indexFromName( mShared->mMapGMLFieldNameToSQLiteColumnName[mShared->mFields.at( i ).name()] );
10381038
if ( idx >= 0 )
10391039
cacheSubSet.append( idx );
10401040
idx = mShared->mFields.indexFromName( mShared->mFields.at( i ).name() );
@@ -1048,7 +1048,7 @@ QgsFeatureRequest QgsWFSFeatureIterator::buildRequestCache( int genCounter )
10481048
const auto referencedColumns = mRequest.filterExpression()->referencedColumns();
10491049
for ( const QString &field : referencedColumns )
10501050
{
1051-
int idx = dataProviderFields.indexFromName( field );
1051+
int idx = dataProviderFields.indexFromName( mShared->mMapGMLFieldNameToSQLiteColumnName[field] );
10521052
if ( idx >= 0 && !cacheSubSet.contains( idx ) )
10531053
cacheSubSet.append( idx );
10541054
idx = mShared->mFields.indexFromName( field );
@@ -1262,7 +1262,7 @@ bool QgsWFSFeatureIterator::fetchFeature( QgsFeature &f )
12621262
continue;
12631263
}
12641264

1265-
copyFeature( cachedFeature, f );
1265+
copyFeature( cachedFeature, f, true );
12661266
geometryToDestinationCrs( f, mTransform );
12671267

12681268
// Retrieve the user-visible id from the Spatialite cache database Id
@@ -1356,7 +1356,7 @@ bool QgsWFSFeatureIterator::fetchFeature( QgsFeature &f )
13561356
continue;
13571357
}
13581358

1359-
copyFeature( feat, f );
1359+
copyFeature( feat, f, false );
13601360
return true;
13611361
}
13621362

@@ -1443,7 +1443,7 @@ bool QgsWFSFeatureIterator::close()
14431443
}
14441444

14451445

1446-
void QgsWFSFeatureIterator::copyFeature( const QgsFeature &srcFeature, QgsFeature &dstFeature )
1446+
void QgsWFSFeatureIterator::copyFeature( const QgsFeature &srcFeature, QgsFeature &dstFeature, bool srcIsCache )
14471447
{
14481448
//copy the geometry
14491449
QgsGeometry geometry = srcFeature.geometry();
@@ -1464,7 +1464,7 @@ void QgsWFSFeatureIterator::copyFeature( const QgsFeature &srcFeature, QgsFeatur
14641464

14651465
auto setAttr = [ & ]( const int i )
14661466
{
1467-
int idx = srcFeature.fields().indexFromName( fields.at( i ).name() );
1467+
int idx = srcFeature.fields().indexFromName( srcIsCache ? mShared->mMapGMLFieldNameToSQLiteColumnName[fields.at( i ).name()] : fields.at( i ).name() );
14681468
if ( idx >= 0 )
14691469
{
14701470
const QVariant &v = srcFeature.attributes().value( idx );

‎src/providers/wfs/qgswfsfeatureiterator.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ class QgsWFSFeatureIterator : public QObject,
227227
bool fetchFeature( QgsFeature &f ) override;
228228

229229
//! Copies feature attributes / geometry from srcFeature to dstFeature
230-
void copyFeature( const QgsFeature &srcFeature, QgsFeature &dstFeature );
230+
void copyFeature( const QgsFeature &srcFeature, QgsFeature &dstFeature, bool srcIsCache );
231231

232232
std::shared_ptr<QgsWFSSharedData> mShared; //!< Mutable data shared between provider and feature sources
233233

‎src/providers/wfs/qgswfsshareddata.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
#include <cpl_conv.h>
3434
#include <ogr_api.h>
3535

36+
#include <set>
37+
3638
#include <sqlite3.h>
3739

3840
QgsWFSSharedData::QgsWFSSharedData( const QString &uri )
@@ -231,6 +233,7 @@ bool QgsWFSSharedData::createCache()
231233
Q_ASSERT( !QFile::exists( mCacheDbname ) );
232234

233235
QgsFields cacheFields;
236+
std::set<QString> setSQLiteColumnNameUpperCase;
234237
for ( const QgsField &field : qgis::as_const( mFields ) )
235238
{
236239
QVariant::Type type = field.type();
@@ -241,7 +244,19 @@ bool QgsWFSSharedData::createCache()
241244
// it to a String
242245
type = QVariant::LongLong;
243246
}
244-
cacheFields.append( QgsField( field.name(), type, field.typeName() ) );
247+
248+
// Make sure we don't have several field names that only differ by their case
249+
QString sqliteFieldName( field.name() );
250+
int counter = 2;
251+
while ( setSQLiteColumnNameUpperCase.find( sqliteFieldName.toUpper() ) != setSQLiteColumnNameUpperCase.end() )
252+
{
253+
sqliteFieldName = field.name() + QStringLiteral( "%1" ).arg( counter );
254+
counter++;
255+
}
256+
setSQLiteColumnNameUpperCase.insert( sqliteFieldName.toUpper() );
257+
mMapGMLFieldNameToSQLiteColumnName[field.name()] = sqliteFieldName;
258+
259+
cacheFields.append( QgsField( sqliteFieldName, type, field.typeName() ) );
245260
}
246261
// Add some field for our internal use
247262
cacheFields.append( QgsField( QgsWFSConstants::FIELD_GEN_COUNTER, QVariant::Int, QStringLiteral( "int" ) ) );
@@ -385,6 +400,7 @@ bool QgsWFSSharedData::createCache()
385400
{
386401
mCacheTablename = QStringLiteral( "features" );
387402
sql = QStringLiteral( "CREATE TABLE %1 (%2 INTEGER PRIMARY KEY" ).arg( mCacheTablename, fidName );
403+
388404
for ( const QgsField &field : qgis::as_const( cacheFields ) )
389405
{
390406
QString type( QStringLiteral( "VARCHAR" ) );
@@ -394,6 +410,7 @@ bool QgsWFSSharedData::createCache()
394410
type = QStringLiteral( "BIGINT" );
395411
else if ( field.type() == QVariant::Double )
396412
type = QStringLiteral( "REAL" );
413+
397414
sql += QStringLiteral( ", %1 %2" ).arg( quotedIdentifier( field.name() ), type );
398415
}
399416
sql += QLatin1String( ")" );
@@ -869,7 +886,7 @@ bool QgsWFSSharedData::changeAttributeValues( const QgsChangedAttributesMap &att
869886
QgsAttributeMap newAttrMap;
870887
for ( QgsAttributeMap::const_iterator siter = attrs.begin(); siter != attrs.end(); ++siter )
871888
{
872-
int idx = dataProviderFields.indexFromName( mFields.at( siter.key() ).name() );
889+
int idx = dataProviderFields.indexFromName( mMapGMLFieldNameToSQLiteColumnName[mFields.at( siter.key() ).name()] );
873890
Q_ASSERT( idx >= 0 );
874891
if ( siter.value().type() == QVariant::DateTime && !siter.value().isNull() )
875892
newAttrMap[idx] = QVariant( siter.value().toDateTime().toMSecsSinceEpoch() );
@@ -988,7 +1005,7 @@ void QgsWFSSharedData::serializeFeatures( QVector<QgsWFSFeatureGmlIdPair> &featu
9881005
//and the attributes
9891006
for ( int i = 0; i < mFields.size(); i++ )
9901007
{
991-
int idx = dataProviderFields.indexFromName( mFields.at( i ).name() );
1008+
int idx = dataProviderFields.indexFromName( mMapGMLFieldNameToSQLiteColumnName[mFields.at( i ).name()] );
9921009
if ( idx >= 0 )
9931010
{
9941011
const QVariant &v = gmlFeature.attributes().value( i );

‎src/providers/wfs/qgswfsshareddata.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#include "qgsogcutils.h"
2323
#include "qgssqliteutils.h"
2424

25+
#include <map>
26+
2527
/**
2628
* This class holds data, and logic, shared between QgsWFSProvider, QgsWFSFeatureIterator
2729
* and QgsWFSFeatureDownloader. It manages the on-disk cache, as a SpatiaLite
@@ -240,6 +242,10 @@ class QgsWFSSharedData : public QObject
240242
//! Tablename of the on-disk cache
241243
QString mCacheTablename;
242244

245+
//! Map each GML field name to the column name in the spatialite DB cache
246+
// This is useful when there are GML fields with same name, but different case
247+
std::map<QString, QString> mMapGMLFieldNameToSQLiteColumnName;
248+
243249
//! Spatial index of requested cached regions
244250
QgsSpatialIndex mCachedRegions;
245251

‎tests/src/python/test_provider_wfs.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4018,6 +4018,100 @@ def testFilteredFeatureRequests(self):
40184018
req.setExpressionContext(ctx)
40194019
qgis_feat = next(vl.getFeatures(req))
40204020

4021+
def testWFSFieldWithSameNameButDifferentCase(self):
4022+
"""Test a layer with field foo and FOO """
4023+
4024+
endpoint = self.__class__.basetestpath + '/fake_qgis_http_endpoint_FieldWithSameNameButDifferentCase'
4025+
4026+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetCapabilities&VERSION=1.0.0'), 'wb') as f:
4027+
f.write("""
4028+
<WFS_Capabilities version="1.0.0" xmlns="http://www.opengis.net/wfs" xmlns:ogc="http://www.opengis.net/ogc">
4029+
<FeatureTypeList>
4030+
<FeatureType>
4031+
<Name>my:typename</Name>
4032+
<Title>Title</Title>
4033+
<Abstract>Abstract</Abstract>
4034+
<SRS>EPSG:32631</SRS>
4035+
<!-- in WFS 1.0, LatLongBoundingBox is in SRS units, not necessarily lat/long... -->
4036+
<LatLongBoundingBox minx="400000" miny="5400000" maxx="450000" maxy="5500000"/>
4037+
</FeatureType>
4038+
</FeatureTypeList>
4039+
</WFS_Capabilities>""".encode('UTF-8'))
4040+
4041+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.0.0&TYPENAME=my:typename'), 'wb') as f:
4042+
f.write("""
4043+
<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">
4044+
<xsd:import namespace="http://www.opengis.net/gml"/>
4045+
<xsd:complexType name="typenameType">
4046+
<xsd:complexContent>
4047+
<xsd:extension base="gml:AbstractFeatureType">
4048+
<xsd:sequence>
4049+
<xsd:element maxOccurs="1" minOccurs="0" name="foo" nillable="true" type="xsd:int"/>
4050+
<xsd:element maxOccurs="1" minOccurs="0" name="FOO" nillable="true" type="xsd:int"/>
4051+
<xsd:element maxOccurs="1" minOccurs="0" name="FOO2" nillable="true" type="xsd:int"/>
4052+
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:PointPropertyType"/>
4053+
</xsd:sequence>
4054+
</xsd:extension>
4055+
</xsd:complexContent>
4056+
</xsd:complexType>
4057+
<xsd:element name="typename" substitutionGroup="gml:_Feature" type="my:typenameType"/>
4058+
</xsd:schema>
4059+
""".encode('UTF-8'))
4060+
4061+
with open(sanitize(endpoint, '?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.0.0&TYPENAME=my:typename&SRSNAME=EPSG:32631'), 'wb') as f:
4062+
f.write("""
4063+
<wfs:FeatureCollection
4064+
xmlns:wfs="http://www.opengis.net/wfs"
4065+
xmlns:gml="http://www.opengis.net/gml"
4066+
xmlns:my="http://my">
4067+
<gml:boundedBy><gml:null>unknown</gml:null></gml:boundedBy>
4068+
<gml:featureMember>
4069+
<my:typename fid="typename.0">
4070+
<my:foo>1</my:foo>
4071+
<my:FOO>2</my:FOO>
4072+
<my:FOO2>3</my:FOO2>
4073+
</my:typename>
4074+
</gml:featureMember>
4075+
</wfs:FeatureCollection>""".encode('UTF-8'))
4076+
4077+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
4078+
self.assertTrue(vl.isValid())
4079+
self.assertEqual(len(vl.fields()), 3)
4080+
4081+
values = [f['foo'] for f in vl.getFeatures()]
4082+
self.assertEqual(values, [1])
4083+
4084+
values = [f['FOO'] for f in vl.getFeatures()]
4085+
self.assertEqual(values, [2])
4086+
4087+
values = [f['FOO2'] for f in vl.getFeatures()]
4088+
self.assertEqual(values, [3])
4089+
4090+
# Also test that on file iterator works
4091+
os.environ['QGIS_WFS_ITERATOR_TRANSFER_THRESHOLD'] = '0'
4092+
4093+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
4094+
values = [f['foo'] for f in vl.getFeatures()]
4095+
self.assertEqual(values, [1])
4096+
4097+
values = [f['FOO'] for f in vl.getFeatures()]
4098+
self.assertEqual(values, [2])
4099+
4100+
values = [f['FOO2'] for f in vl.getFeatures()]
4101+
self.assertEqual(values, [3])
4102+
4103+
del os.environ['QGIS_WFS_ITERATOR_TRANSFER_THRESHOLD']
4104+
4105+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
4106+
request = QgsFeatureRequest().setFilterExpression('FOO = 2')
4107+
values = [f['FOO'] for f in vl.getFeatures(request)]
4108+
self.assertEqual(values, [2])
4109+
4110+
vl = QgsVectorLayer("url='http://" + endpoint + "' typename='my:typename' version='1.0.0'", 'test', 'WFS')
4111+
request = QgsFeatureRequest().setSubsetOfAttributes(['FOO'], vl.fields())
4112+
values = [f['FOO'] for f in vl.getFeatures(request)]
4113+
self.assertEqual(values, [2])
4114+
40214115

40224116
if __name__ == '__main__':
40234117
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.