Skip to content
Permalink
Browse files

Server WFS NULL values support

- expose nillable in describefeaturetype
- serve xsi:nil="true" in getfeature
- check for NULL in transactions and report an error

Fixes #20961  - plus some other unreported
  • Loading branch information
elpaso committed Jan 23, 2019
1 parent 847e7ef commit 71e01168fe43a95002674baf07b3b9b4828b5a08
Showing with 322 additions and 272 deletions.
  1. +6 −1 src/server/services/wfs/qgswfsdescribefeaturetype.cpp
  2. +10 −8 src/server/services/wfs/qgswfsgetfeature.cpp
  3. +1 −0 src/server/services/wfs/qgswfstransaction.cpp
  4. +35 −11 src/server/services/wfs/qgswfstransaction_1_0_0.cpp
  5. +47 −29 tests/src/python/test_qgsserver_wfs.py
  6. BIN tests/testdata/qgis_server/test_project_wms_grouped_layers.gpkg
  7. +22 −22 tests/testdata/qgis_server/wfs_describeFeatureType_1_0_0_typename_as_areas_1_0_0.txt
  8. +62 −62 tests/testdata/qgis_server/wfs_describeFeatureType_1_0_0_typename_empty_1_0_0.txt
  9. +22 −22 tests/testdata/qgis_server/wfs_describeFeatureType_1_1_0_typename_as_areas.txt
  10. +62 −62 tests/testdata/qgis_server/wfs_describeFeatureType_1_1_0_typename_empty.txt
  11. +4 −4 tests/testdata/qgis_server/wfs_describefeaturetype.txt
  12. +4 −4 tests/testdata/qgis_server/wfs_describefeaturetype_1_0_0.txt
  13. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_EXP_FILTER_FID_one_1_0_0.txt
  14. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_EXP_FILTER_gml_bbox_one_1_0_0.txt
  15. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_EXP_FILTER_gml_bbox_three_1_0_0.txt
  16. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_EXP_FILTER_two_1_0_0.txt
  17. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_epsgbbox_1_feature_1_0_0.txt
  18. +2 −2 tests/testdata/qgis_server/wfs_getFeature_1_0_0_epsgbbox_1_feature_3857_1_0_0.txt
  19. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_0_0_epsgbbox_3_feature_1_0_0.txt
  20. +2 −2 tests/testdata/qgis_server/wfs_getFeature_1_0_0_epsgbbox_3_feature_3857_1_0_0.txt
  21. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_1_0_epsgbbox_1_feature.txt
  22. +2 −2 tests/testdata/qgis_server/wfs_getFeature_1_1_0_epsgbbox_1_feature_3857.txt
  23. +1 −1 tests/testdata/qgis_server/wfs_getFeature_1_1_0_epsgbbox_3_feature.txt
  24. +2 −2 tests/testdata/qgis_server/wfs_getFeature_1_1_0_epsgbbox_3_feature_3857.txt
  25. +9 −9 tests/testdata/qgis_server/wfs_getcapabilities.txt
  26. +7 −7 tests/testdata/qgis_server/wfs_getcapabilities_1_0_0.txt
  27. +1 −1 tests/testdata/qgis_server/wfs_getfeature_bbox_inside_and_post.txt
  28. +1 −1 tests/testdata/qgis_server/wfs_getfeature_hits.txt
  29. +1 −1 tests/testdata/qgis_server/wfs_getfeature_limit2.txt
  30. +1 −1 tests/testdata/qgis_server/wfs_getfeature_limit2_post.txt
  31. +1 −1 tests/testdata/qgis_server/wfs_getfeature_nobbox.txt
  32. +1 −1 tests/testdata/qgis_server/wfs_getfeature_nobbox_post.txt
  33. +1 −1 tests/testdata/qgis_server/wfs_getfeature_sortby.txt
  34. +1 −1 tests/testdata/qgis_server/wfs_getfeature_sortby_post.txt
  35. +1 −1 tests/testdata/qgis_server/wfs_getfeature_srs_two_layers_post.txt
  36. +1 −1 tests/testdata/qgis_server/wfs_getfeature_srsname.txt
  37. +1 −1 tests/testdata/qgis_server/wfs_getfeature_srsname_post.txt
  38. +1 −1 tests/testdata/qgis_server/wfs_getfeature_start1_limit1.txt
  39. +1 −1 tests/testdata/qgis_server/wfs_getfeature_start1_limit1_post.txt
  40. +1 −1 tests/testdata/qgis_server/wfs_getfeature_startindex2.txt
  41. +1 −1 tests/testdata/qgis_server/wfs_getfeature_startindex2_post.txt
@@ -79,7 +79,7 @@ namespace QgsWfs
// test oFormat
if ( oFormat == QgsWfsParameters::Format::NONE )
throw QgsBadRequestException( QStringLiteral( "Invalid WFS Parameter" ),
"OUTPUTFORMAT " + wfsParameters.outputFormatAsString() + "is not supported" );
QStringLiteral( "OUTPUTFORMAT %1 is not supported" ).arg( wfsParameters.outputFormatAsString() ) );

QgsAccessControl *accessControl = serverIface->accessControls();

@@ -346,6 +346,11 @@ namespace QgsWfs
}
}

if ( !( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull ) )
{
attElem.setAttribute( QStringLiteral( "nillable" ), QStringLiteral( "true" ) );
}

sequenceElem.appendChild( attElem );

QString alias = field.alias();
@@ -1334,16 +1334,16 @@ namespace QgsWfs
{
continue;
}
if ( featureAttributes[idx].isNull() )
{
continue;
}
const QgsField field = fields.at( idx );
const QgsEditorWidgetSetup setup = field.editorWidgetSetup();
QString attributeName = field.name();

QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
if ( featureAttributes[idx].isNull() )
{
fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
}
fieldElem.appendChild( fieldText );
typeNameElement.appendChild( fieldElem );
}
@@ -1435,16 +1435,18 @@ namespace QgsWfs
{
continue;
}
if ( featureAttributes[idx].isNull() )
{
continue;
}

const QgsField field = fields.at( idx );
const QgsEditorWidgetSetup setup = field.editorWidgetSetup();

QString attributeName = field.name();

QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) );
QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) );
if ( featureAttributes[idx].isNull() )
{
fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) );
}
fieldElem.appendChild( fieldText );
typeNameElement.appendChild( fieldElem );
}
@@ -411,6 +411,7 @@ namespace QgsWfs
{
if ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull )
{
action.error = true;
action.errorMsg = QStringLiteral( "NOT NULL constraint error error on layer '%1', field '%2'" ).arg( typeName, field.name() );
vlayer->rollBack();
break;
@@ -388,26 +388,50 @@ namespace QgsWfs
}
QgsField field = fields.at( fieldMapIt.value() );
QVariant value = it.value();
if ( field.type() == 2 )
if ( value.isNull() )
{
value = it.value().toInt( &conversionSuccess );
if ( !conversionSuccess )
if ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
action.errorMsg = QStringLiteral( "NOT NULL constraint error error on layer '%1', field '%2'" ).arg( typeName, field.name() );
vlayer->rollBack();
break;
}
}
else if ( field.type() == 6 )
else // Not NULL
{
value = it.value().toDouble( &conversionSuccess );
if ( !conversionSuccess )
if ( field.type() == QVariant::Type::Int )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
value = it.value().toInt( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
else if ( field.type() == QVariant::Type::Double )
{
value = it.value().toDouble( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
else if ( field.type() == QVariant::Type::LongLong )
{
value = it.value().toLongLong( &conversionSuccess );
if ( !conversionSuccess )
{
action.error = true;
action.errorMsg = QStringLiteral( "Property conversion error on layer '%1'" ).arg( typeName );
vlayer->rollBack();
break;
}
}
}
vlayer->changeAttributeValue( feature.id(), fieldMapIt.value(), value );
@@ -446,19 +446,19 @@ def test_describeFeatureType(self):
'wfs_describeFeatureType_1_1_0_typename_wrong', project_file=project_file)

def test_getFeatureFeature_0_nulls(self):
"""Test that 0 and null in integer columns are reported correctly: note that WFS does not support NULL but QGIS Server does"""
"""Test that 0 and null in integer columns are reported correctly"""

# Test transactions with 0 and nulls

post_data = b"""<?xml version="1.0" ?>
<wfs:Transaction service="WFS" version="1.1.0"
post_data = """<?xml version="1.0" ?>
<wfs:Transaction service="WFS" version="{version}"
xmlns:ogc="http://www.opengis.net/ogc"
xmlns:wfs="http://www.opengis.net/wfs"
xmlns:gml="http://www.opengis.net/gml">
<wfs:Update typeName="cdb_lines">
<wfs:Property>
<wfs:Name>id_long</wfs:Name>
<wfs:Value>%s</wfs:Value>
<wfs:Name>{field}</wfs:Name>
<wfs:Value>{value}</wfs:Value>
</wfs:Property>
<fes:Filter>
<fes:FeatureId fid="cdb_lines.22"/>
@@ -467,37 +467,55 @@ def test_getFeatureFeature_0_nulls(self):
</wfs:Transaction>
"""

def _round_trip(value):
"""Set a value on fid 22 and long_id field and check it back"""
def _round_trip(value, field, version='1.1.0'):
"""Set a value on fid 22 and field and check it back"""

header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'), QgsServerRequest.PostMethod, post_data % value)
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body)
encoded_data = post_data.format(field=field, value=value, version=version).encode('utf8')
# Strip the field if NULL
if value is None:
encoded_data = encoded_data.replace(b'<wfs:Value>None</wfs:Value>', b'')

header, body = self._execute_request("?MAP=%s&SERVICE=WFS&VERSION=%s" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs', version), QgsServerRequest.PostMethod, encoded_data)
if version == '1.0.0':
self.assertTrue(b'<SUCCESS/>' in body, body)
else:
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body, body)
header, body = self._execute_request("?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=cdb_lines&FEATUREID=cdb_lines.22" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'))
self.assertTrue(b'<qgs:id_long>%s</qgs:id_long>' % value in body)
if value is not None:
xml_value = '<qgs:{0}>{1}</qgs:{0}>'.format(field, value).encode('utf8')
self.assertTrue(xml_value in body, "%s not found in body" % xml_value)
else:
xml_value = '<qgs:{0}>'.format(field).encode('utf8')
self.assertFalse(xml_value in body)
# Check the backend
vl = QgsVectorLayer(
self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=cdb_lines', 'vl', 'ogr')
self.assertTrue(vl.isValid())
self.assertEqual(str(vl.getFeature(22)['id_long']).encode(
'utf8'), value if value != b'' else b'NULL')

_round_trip(b'0')
_round_trip(b'12345')

# Now check NULL
header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (self.testdata_path + 'test_project_wms_grouped_layers.qgs'),
QgsServerRequest.PostMethod, (post_data % b'').replace(b'<wfs:Value></wfs:Value>', b''))
self.assertTrue(b'<TotalUpdated>1</TotalUpdated>' in body)
vl = QgsVectorLayer(
self.testdata_path + 'test_project_wms_grouped_layers.gpkg|layername=cdb_lines', 'vl', 'ogr')
self.assertTrue(vl.isValid())
self.assertTrue(vl.getFeature(22)['id_long'].isNull())
del(vl)
header, body = self._execute_request("?MAP=%s&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=cdb_lines&FEATUREID=cdb_lines.22" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'))
self.assertFalse(b'<qgs:id_long></qgs:id_long>' in body)
self.assertEqual(
str(vl.getFeature(22)[field]), value if value is not None else 'NULL')

for version in ('1.0.0', '1.1.0'):
_round_trip('0', 'id_long', version)
_round_trip('12345', 'id_long', version)
_round_trip('0', 'id', version)
_round_trip('12345', 'id', version)
_round_trip(None, 'id', version)
_round_trip(None, 'id_long', version)

# "name" is NOT NULL: try to set it to empty string
_round_trip('', 'name', version)
# Then NULL
data = post_data.format(field='name', value='', version=version).encode('utf8')
encoded_data = data.replace(b'<wfs:Value></wfs:Value>', b'')
header, body = self._execute_request("?MAP=%s&SERVICE=WFS" % (
self.testdata_path + 'test_project_wms_grouped_layers.qgs'), QgsServerRequest.PostMethod, encoded_data)
if version == '1.0.0':
self.assertTrue(b'<ERROR/>' in body, body)
else:
self.assertTrue(b'<TotalUpdated>0</TotalUpdated>' in body)
self.assertTrue(b'<Message>NOT NULL constraint error error on layer \'cdb_lines\', field \'name\'</Message>' in body)


if __name__ == '__main__':
Binary file not shown.
@@ -1,4 +1,4 @@
Content-Length: 1840
Content-Length: 2176
Content-Type: text/xml; charset=utf-8

<schema xmlns:gml="http://www.opengis.net/gml" targetNamespace="http://www.qgis.org/gml" xmlns:qgs="http://www.qgis.org/gml" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ogc="http://www.opengis.net/ogc" version="1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
@@ -10,27 +10,27 @@ Content-Type: text/xml; charset=utf-8
<sequence>
<element maxOccurs="1" type="gml:MultiPolygonPropertyType" minOccurs="0" name="geometry"/>
<element type="long" name="fid"/>
<element type="int" name="gid"/>
<element type="string" name="datum"/>
<element type="string" name="bearbeiter"/>
<element type="string" name="veranstaltung"/>
<element type="string" name="beschriftung"/>
<element type="string" name="name"/>
<element type="string" name="flaechentyp"/>
<element type="string" name="farbe"/>
<element type="string" name="schraff_width"/>
<element type="string" name="schraff_width_prt"/>
<element type="string" name="schraff_size"/>
<element type="string" name="schraff_size_prt"/>
<element type="string" name="schraff_winkel"/>
<element type="string" name="umrissfarbe"/>
<element type="string" name="umrisstyp"/>
<element type="string" name="umrissstaerke"/>
<element type="string" name="umrissstaerke_prt"/>
<element type="decimal" name="umfang"/>
<element type="decimal" name="flaeche"/>
<element type="string" name="bemerkung"/>
<element type="string" name="last_change"/>
<element type="int" nillable="true" name="gid"/>
<element type="string" nillable="true" name="datum"/>
<element type="string" nillable="true" name="bearbeiter"/>
<element type="string" nillable="true" name="veranstaltung"/>
<element type="string" nillable="true" name="beschriftung"/>
<element type="string" nillable="true" name="name"/>
<element type="string" nillable="true" name="flaechentyp"/>
<element type="string" nillable="true" name="farbe"/>
<element type="string" nillable="true" name="schraff_width"/>
<element type="string" nillable="true" name="schraff_width_prt"/>
<element type="string" nillable="true" name="schraff_size"/>
<element type="string" nillable="true" name="schraff_size_prt"/>
<element type="string" nillable="true" name="schraff_winkel"/>
<element type="string" nillable="true" name="umrissfarbe"/>
<element type="string" nillable="true" name="umrisstyp"/>
<element type="string" nillable="true" name="umrissstaerke"/>
<element type="string" nillable="true" name="umrissstaerke_prt"/>
<element type="decimal" nillable="true" name="umfang"/>
<element type="decimal" nillable="true" name="flaeche"/>
<element type="string" nillable="true" name="bemerkung"/>
<element type="string" nillable="true" name="last_change"/>
</sequence>
</extension>
</complexContent>

0 comments on commit 71e0116

Please sign in to comment.
You can’t perform that action at this time.