Skip to content

Commit

Permalink
WFS-T 1.1.0 client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso committed Oct 13, 2020
1 parent a3d6965 commit b1da081
Show file tree
Hide file tree
Showing 9 changed files with 727 additions and 12 deletions.
103 changes: 91 additions & 12 deletions src/providers/wfs/qgswfsprovider.cpp
Expand Up @@ -1264,6 +1264,8 @@ bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields

QByteArray response = describeFeatureType.response();

QgsDebugMsgLevel( response, 4 );

QDomDocument describeFeatureDocument;
QString errorMsg;
if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
Expand Down Expand Up @@ -1580,17 +1582,25 @@ bool QgsWFSProvider::sendTransactionDocument( const QDomDocument &doc, QDomDocum
return false;
}

QgsDebugMsgLevel( doc.toString(), 4 );

QgsWFSTransactionRequest request( mShared->mURI );
return request.send( doc, serverResponse );
}

QDomElement QgsWFSProvider::createTransactionElement( QDomDocument &doc ) const
{
QDomElement transactionElem = doc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Transaction" ) );
// QString WfsVersion = mShared->mWFSVersion;
// For now: hardcoded to 1.0.0
QString WfsVersion = QStringLiteral( "1.0.0" );
transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion );
const QString WfsVersion = mShared->mWFSVersion;
// only 1.1.0 and 1.0.0 are supported
if ( WfsVersion == QStringLiteral( "1.1.0" ) )
{
transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion );
}
else
{
transactionElem.setAttribute( QStringLiteral( "version" ), QStringLiteral( "1.0.0" ) );
}
transactionElem.setAttribute( QStringLiteral( "service" ), QStringLiteral( "WFS" ) );
transactionElem.setAttribute( QStringLiteral( "xmlns:xsi" ), QStringLiteral( "http://www.w3.org/2001/XMLSchema-instance" ) );

Expand Down Expand Up @@ -1634,19 +1644,70 @@ bool QgsWFSProvider::transactionSuccess( const QDomDocument &serverResponse ) co
return false;
}

QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) );
if ( transactionResultList.size() < 1 )
const QString WfsVersion = mShared->mWFSVersion;

if ( WfsVersion == QStringLiteral( "1.1.0" ) )
{
const QDomNodeList transactionSummaryList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionSummary" ) );
if ( transactionSummaryList.size() < 1 )
{
return false;
}

QDomElement transactionElement { transactionSummaryList.at( 0 ).toElement() };
QDomNodeList totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalInserted" ) );
QDomNodeList totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalUpdated" ) );
QDomNodeList totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalDeleted" ) );
if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}
if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}
if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}

// Handle wrong QGIS server response (capital initial letter)
totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalInserted" ) );
totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalUpdated" ) );
totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalDeleted" ) );
if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}
if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}
if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
{
return true;
}

return false;
}

QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) );
if ( statusList.size() < 1 )
}
else
{
return false;
const QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) );
if ( transactionResultList.size() < 1 )
{
return false;
}

const QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) );
if ( statusList.size() < 1 )
{
return false;
}

return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" );
}

return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" );
}

QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverResponse ) const
Expand All @@ -1663,7 +1724,17 @@ QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverRespon
return ids;
}

QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "InsertResult" ) );
// Handles WFS 1.1.0
QString insertResultTagName;
if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) )
{
insertResultTagName = QStringLiteral( "InsertResults" );
}
else
{
insertResultTagName = QStringLiteral( "InsertResult" );
}
QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, insertResultTagName );
for ( int i = 0; i < insertResultList.size(); ++i )
{
QDomNodeList featureIdList = insertResultList.at( i ).toElement().elementsByTagNameNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
Expand Down Expand Up @@ -1850,6 +1921,14 @@ void QgsWFSProvider::handleException( const QDomDocument &serverResponse )
return;
}

// WFS 1.1.0
if ( exceptionElem.tagName() == QLatin1String( "TransactionResponse" ) )
{
pushError( tr( "Unsuccessful service response: no features were added, deleted or changed." ) );
return;
}


if ( exceptionElem.tagName() == QLatin1String( "ExceptionReport" ) )
{
QDomElement exception = exceptionElem.firstChildElement( QStringLiteral( "Exception" ) );
Expand Down
72 changes: 72 additions & 0 deletions tests/src/python/test_provider_wfs.py
Expand Up @@ -43,6 +43,9 @@
unittest
)
from providertestbase import ProviderTestCase
from utilities import unitTestDataPath

TEST_DATA_DIR = unitTestDataPath()


def sanitize(endpoint, x):
Expand Down Expand Up @@ -4705,6 +4708,75 @@ def testGetCapabilitiesReturnWMSException(self):
self.assertEqual(len(logger.messages()), 1, logger.messages())
self.assertTrue("InvalidFormat: Can't recognize service requested." in logger.messages()[0].decode('UTF-8'), logger.messages())

def testWFST11(self):
"""Test WFS-T 1.1 (read-write)"""

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

shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'getcapabilities.xml'), sanitize(endpoint, '?SERVICE=WFS?REQUEST=GetCapabilities?VERSION=1.1.0'))
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'describefeaturetype_polygons.xml'), sanitize(endpoint, '?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=1.1.0&TYPENAME=ws1:polygons'))

vl = QgsVectorLayer("url='http://" + endpoint + "' typename='ws1:polygons' version='1.1.0'", 'test', 'WFS')
self.assertTrue(vl.isValid())
self.assertEqual(vl.featureCount(), 0)

self.assertEqual(vl.dataProvider().capabilities(),
QgsVectorDataProvider.AddFeatures
| QgsVectorDataProvider.ChangeAttributeValues
| QgsVectorDataProvider.ChangeGeometries
| QgsVectorDataProvider.DeleteFeatures
| QgsVectorDataProvider.SelectAtId)

# Transaction response failure (no modifications)
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_empty.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" xsi:schemaLocation="ws1 http://localhost:8600/geoserver/ws1/wfs?SERVICE=WFS&amp;REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=ws1:polygons" version="1.1.0" service="WFS"><Insert xmlns="http://www.opengis.net/wfs"><polygons xmlns="ws1"/></Insert></Transaction>'))

(ret, _) = vl.dataProvider().addFeatures([QgsFeature()])
self.assertFalse(ret)

self.assertEqual(vl.featureCount(), 0)

self.assertFalse(vl.dataProvider().deleteFeatures([0]))

self.assertEqual(vl.featureCount(), 0)

self.assertFalse(vl.dataProvider().changeGeometryValues({0: QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))')}))

self.assertFalse(vl.dataProvider().changeAttributeValues({0: {0: 0}}))

# Test add features for real
# Transaction response with 1 feature added
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_added.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" xsi:schemaLocation="ws1 http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=ws1:polygons" version="1.1.0" service="WFS"><Insert xmlns="http://www.opengis.net/wfs"><polygons xmlns="ws1"><name xmlns="ws1">one</name><value xmlns="ws1">1</value><geometry xmlns="ws1"><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates cs="," ts=" ">9,45 10,45 10,46 9,46 9,45</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></geometry></polygons></Insert></Transaction>'))

feat = QgsFeature(vl.fields())
feat.setAttribute('name', 'one')
feat.setAttribute('value', 1)
feat.setGeometry(QgsGeometry.fromWkt('Polygon ((9 45, 10 45, 10 46, 9 46, 9 45))'))
(ret, features) = vl.dataProvider().addFeatures([feat])
self.assertEqual(features[0].attributes(), ['one', 1])
self.assertEqual(vl.featureCount(), 1)

# Test change attributes
# Transaction response with 1 feature changed
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" xsi:schemaLocation="ws1 http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=ws1:polygons" version="1.1.0" service="WFS"><Update xmlns="http://www.opengis.net/wfs" typeName="ws1:polygons"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">ws1:name</Name><Value xmlns="http://www.opengis.net/wfs">one-one-one</Value></Property><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">ws1:value</Name><Value xmlns="http://www.opengis.net/wfs">111</Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="123"/></Filter></Update></Transaction>'))

self.assertTrue(vl.dataProvider().changeAttributeValues({1: {0: 'one-one-one', 1: 111}}))
self.assertEqual(next(vl.dataProvider().getFeatures()).attributes(), ['one-one-one', 111])

# Test change geometry
# Transaction response with 1 feature changed
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_changed.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" xsi:schemaLocation="ws1 http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=ws1:polygons" version="1.1.0" service="WFS"><Update xmlns="http://www.opengis.net/wfs" typeName="ws1:polygons"><Property xmlns="http://www.opengis.net/wfs"><Name xmlns="http://www.opengis.net/wfs">ws1:geometry</Name><Value xmlns="http://www.opengis.net/wfs"><gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates cs="," ts=" ">10,46 11,46 11,47 10,47 10,46</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon></Value></Property><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="123"/></Filter></Update></Transaction>'))

new_geom = QgsGeometry.fromWkt('Polygon ((10 46, 11 46, 11 47, 10 47, 10 46))')
self.assertTrue(vl.dataProvider().changeGeometryValues({1: new_geom}))
self.assertEqual(next(vl.dataProvider().getFeatures()).geometry().asWkt(), new_geom.asWkt())

# Test delete feature
# Transaction response with 1 feature deleted
shutil.copy(os.path.join(TEST_DATA_DIR, 'provider', 'wfst-1-1', 'transaction_response_feature_deleted.xml'), sanitize(endpoint, '?SERVICE=WFS&POSTDATA=<Transaction xmlns="http://www.opengis.net/wfs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" xsi:schemaLocation="ws1 http://fake_qgis_http_endpoint?REQUEST=DescribeFeatureType&amp;VERSION=1.0.0&amp;TYPENAME=ws1:polygons" version="1.1.0" service="WFS"><Delete xmlns="http://www.opengis.net/wfs" typeName="ws1:polygons"><Filter xmlns="http://www.opengis.net/ogc"><FeatureId xmlns="http://www.opengis.net/ogc" fid="123"/></Filter></Delete></Transaction>'))

self.assertTrue(vl.dataProvider().deleteFeatures([1]))
self.assertEqual(vl.featureCount(), 0)


if __name__ == '__main__':
unittest.main()
15 changes: 15 additions & 0 deletions tests/testdata/provider/wfst-1-1/describefeaturetype_polygons.xml
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:gml="http://www.opengis.net/gml" xmlns:ws1="ws1" elementFormDefault="qualified" targetNamespace="ws1">
<xsd:import namespace="http://www.opengis.net/gml" schemaLocation="http://localhost:8600/geoserver/schemas/gml/3.1.1/base/gml.xsd"/>
<xsd:complexType name="polygonsType">
<xsd:complexContent>
<xsd:extension base="gml:AbstractFeatureType">
<xsd:sequence>
<xsd:element maxOccurs="1" minOccurs="0" name="geometry" nillable="true" type="gml:SurfacePropertyType"/>
<xsd:element maxOccurs="1" minOccurs="0" name="name" nillable="true" type="xsd:string"/>
<xsd:element maxOccurs="1" minOccurs="0" name="value" nillable="true" type="xsd:int"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:element name="polygons" substitutionGroup="gml:_Feature" type="ws1:polygonsType"/>
</xsd:schema>

0 comments on commit b1da081

Please sign in to comment.