1 /***************************************************************************
2 qgswfsprovider.cpp
3 -------------------
4 begin : July 25, 2006
5 copyright : (C) 2006 by Marco Hugentobler
6 email : marco dot hugentobler at karto dot baug dot ethz dot ch
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include "qgis.h"
19 #include "qgsfeature.h"
20 #include "qgsfields.h"
21 #include "qgsgeometry.h"
22 #include "qgscoordinatereferencesystem.h"
23 #include "qgslogger.h"
24 #include "qgsmessagelog.h"
25 #include "qgsogcutils.h"
26 #include "qgsoapifprovider.h"
27 #include "qgswfsdataitems.h"
28 #include "qgswfsconstants.h"
29 #include "qgswfsfeatureiterator.h"
30 #include "qgswfsprovider.h"
31 #include "qgswfscapabilities.h"
32 #include "qgswfsdescribefeaturetype.h"
33 #include "qgswfstransactionrequest.h"
34 #include "qgswfsshareddata.h"
35 #include "qgswfsutils.h"
36 #include "qgssettings.h"
37
38 #include <QApplication>
39 #include <QDomDocument>
40 #include <QMessageBox>
41 #include <QDomNodeList>
42 #include <QNetworkRequest>
43 #include <QNetworkReply>
44 #include <QFile>
45 #include <QUrl>
46 #include <QWidget>
47 #include <QPair>
48 #include <QTimer>
49
50 #include <cfloat>
51
52 const QString QgsWFSProvider::WFS_PROVIDER_KEY = QStringLiteral( "WFS" );
53 const QString QgsWFSProvider::WFS_PROVIDER_DESCRIPTION = QStringLiteral( "WFS data provider" );
54
55
QgsWFSProvider(const QString & uri,const ProviderOptions & options,const QgsWfsCapabilities::Capabilities & caps)56 QgsWFSProvider::QgsWFSProvider( const QString &uri, const ProviderOptions &options, const QgsWfsCapabilities::Capabilities &caps )
57 : QgsVectorDataProvider( uri, options )
58 , mShared( new QgsWFSSharedData( uri ) )
59 {
60 mShared->mCaps = caps;
61 mShared->mServerMaxFeatures = caps.maxFeatures;
62
63 connect( mShared.get(), &QgsWFSSharedData::raiseError, this, &QgsWFSProvider::pushErrorSlot );
64 connect( mShared.get(), &QgsWFSSharedData::extentUpdated, this, &QgsWFSProvider::fullExtentCalculated );
65
66 if ( uri.isEmpty() )
67 {
68 mValid = false;
69 return;
70 }
71
72 //create mSourceCrs from url if possible [WBC 111221] refactored from GetFeatureGET()
73 QString srsname = mShared->mURI.SRSName();
74 if ( !srsname.isEmpty() )
75 {
76 if ( srsname == QLatin1String( "EPSG:900913" ) )
77 mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "EPSG:3857" ) );
78 else
79 mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( srsname );
80 }
81
82 // Must be called first to establish the version, in case we are in auto-detection
83 if ( !getCapabilities() )
84 {
85 mValid = false;
86 return;
87 }
88
89 if ( !mShared->mURI.sql().isEmpty() )
90 {
91 if ( !processSQL( mShared->mURI.sql(), mProcessSQLErrorMsg, mProcessSQLWarningMsg ) )
92 {
93 QgsMessageLog::logMessage( mProcessSQLErrorMsg, tr( "WFS" ) );
94 mValid = false;
95 return;
96 }
97 mSubsetString = mShared->mURI.sql();
98 }
99 else
100 {
101 mSubsetString = mShared->mURI.filter();
102
103 //fetch attributes of layer and type of its geometry attribute
104 //WBC 111221: extracting geometry type here instead of getFeature allows successful
105 //layer creation even when no features are retrieved (due to, e.g., BBOX or FILTER)
106 if ( !describeFeatureType( mShared->mGeometryAttribute, mShared->mFields, mShared->mWKBType ) )
107 {
108 mValid = false;
109 return;
110 }
111 mThisTypenameFields = mShared->mFields;
112 }
113
114 if ( !mShared->computeFilter( mProcessSQLErrorMsg ) )
115 {
116 QgsMessageLog::logMessage( mProcessSQLErrorMsg, tr( "WFS" ) );
117 mValid = false;
118 return;
119 }
120
121 const auto GetGeometryTypeFromOneFeature = [&]()
122 {
123 const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
124 auto downloader = qgis::make_unique<QgsFeatureDownloader>();
125 downloader->setImpl( qgis::make_unique<QgsWFSFeatureDownloaderImpl>( mShared.get(), downloader.get(), requestMadeFromMainThread ) );
126 connect( downloader.get(),
127 qgis::overload < QVector<QgsFeatureUniqueIdPair> >::of( &QgsFeatureDownloader::featureReceived ),
128 this, &QgsWFSProvider::featureReceivedAnalyzeOneFeature );
129 if ( requestMadeFromMainThread )
130 {
131 auto processEvents = []()
132 {
133 QApplication::instance()->processEvents();
134 };
135 connect( downloader.get(), &QgsFeatureDownloader::resumeMainThread,
136 this, processEvents );
137 }
138 downloader->run( false, /* serialize features */
139 1 /* maxfeatures */ );
140 };
141
142 //Failed to detect feature type from describeFeatureType -> get first feature from layer to detect type
143 if ( mShared->mWKBType == QgsWkbTypes::Unknown )
144 {
145 GetGeometryTypeFromOneFeature();
146
147 // If we still didn't get the geometry type, and have a filter, temporarily
148 // disable the filter.
149 // See https://github.com/qgis/QGIS/issues/43950
150 if ( mShared->mWKBType == QgsWkbTypes::Unknown && !mSubsetString.isEmpty() )
151 {
152 const QString oldFilter = mShared->setWFSFilter( QString() );
153 GetGeometryTypeFromOneFeature();
154 mShared->setWFSFilter( oldFilter );
155 }
156 }
157 }
158
~QgsWFSProvider()159 QgsWFSProvider::~QgsWFSProvider()
160 {
161 QgsDebugMsgLevel( QStringLiteral( "~QgsWFSProvider()" ), 4 );
162 }
163
164 class QgsWFSProviderSQLFunctionValidator: public QgsSQLStatement::RecursiveVisitor
165 {
166 public:
167 QgsWFSProviderSQLFunctionValidator(
168 const QList<QgsWfsCapabilities::Function> &spatialPredicatesList,
169 const QList<QgsWfsCapabilities::Function> &functionList );
170
hasError() const171 bool hasError() const { return mError; }
172
errorMessage() const173 const QString &errorMessage() const { return mErrorMessage; }
174
175 protected:
176 void visit( const QgsSQLStatement::NodeFunction &n ) override;
177
178 private:
179 const QList<QgsWfsCapabilities::Function> &mSpatialPredicatesList;
180 const QList<QgsWfsCapabilities::Function> &mFunctionList;
181 bool mError;
182 QString mErrorMessage;
183 };
184
QgsWFSProviderSQLFunctionValidator(const QList<QgsWfsCapabilities::Function> & spatialPredicatesList,const QList<QgsWfsCapabilities::Function> & functionList)185 QgsWFSProviderSQLFunctionValidator::QgsWFSProviderSQLFunctionValidator(
186 const QList<QgsWfsCapabilities::Function> &spatialPredicatesList,
187 const QList<QgsWfsCapabilities::Function> &functionList )
188 : mSpatialPredicatesList( spatialPredicatesList )
189 , mFunctionList( functionList )
190 , mError( false )
191 {
192 }
193
visit(const QgsSQLStatement::NodeFunction & n)194 void QgsWFSProviderSQLFunctionValidator::visit( const QgsSQLStatement::NodeFunction &n )
195 {
196 if ( !mError )
197 {
198 bool foundMatch = false;
199 const auto constMSpatialPredicatesList = mSpatialPredicatesList;
200 for ( const QgsWfsCapabilities::Function &f : constMSpatialPredicatesList )
201 {
202 if ( n.name().compare( f.name, Qt::CaseInsensitive ) == 0 ||
203 QString( "ST_" + n.name() ).compare( f.name, Qt::CaseInsensitive ) == 0 )
204 {
205 foundMatch = true;
206 }
207 }
208 const auto constMFunctionList = mFunctionList;
209 for ( const QgsWfsCapabilities::Function &f : constMFunctionList )
210 {
211 if ( n.name().compare( f.name, Qt::CaseInsensitive ) == 0 )
212 {
213 foundMatch = true;
214 }
215 }
216 if ( !foundMatch )
217 {
218 mError = true;
219 mErrorMessage = QObject::tr( "Function '%1' is not declared by the WFS server" ).arg( n.name() );
220 }
221 QgsSQLStatement::RecursiveVisitor::visit( n );
222 }
223 }
224
225 class QgsWFSProviderSQLColumnRefValidator: public QgsSQLStatement::RecursiveVisitor
226 {
227 public:
228 QgsWFSProviderSQLColumnRefValidator(
229 const QgsWfsCapabilities::Capabilities &caps,
230 const QString &defaultTypeName,
231 const QMap< QString, QString > &mapTypenameAliasToTypename,
232 const QMap < QString, QgsFields > &mapTypenameToFields,
233 const QMap < QString, QString > &mapTypenameToGeometryAttribute );
234
hasError() const235 bool hasError() const { return mError; }
236
errorMessage() const237 const QString &errorMessage() const { return mErrorMessage; }
238
239 protected:
240 void visit( const QgsSQLStatement::NodeColumnRef &n ) override;
241
242 private:
243 const QgsWfsCapabilities::Capabilities mCaps;
244 QString mDefaultTypeName;
245 const QMap< QString, QString > &mMapTableAliasToName;
246 const QMap < QString, QgsFields > &mMapTypenameToFields;
247 const QMap < QString, QString > &mMapTypenameToGeometryAttribute;
248 bool mError;
249 QString mErrorMessage;
250 };
251
QgsWFSProviderSQLColumnRefValidator(const QgsWfsCapabilities::Capabilities & caps,const QString & defaultTypeName,const QMap<QString,QString> & mapTypenameAliasToTypename,const QMap<QString,QgsFields> & mapTypenameToFields,const QMap<QString,QString> & mapTypenameToGeometryAttribute)252 QgsWFSProviderSQLColumnRefValidator::QgsWFSProviderSQLColumnRefValidator(
253 const QgsWfsCapabilities::Capabilities &caps,
254 const QString &defaultTypeName,
255 const QMap< QString, QString > &mapTypenameAliasToTypename,
256 const QMap < QString, QgsFields > &mapTypenameToFields,
257 const QMap < QString, QString > &mapTypenameToGeometryAttribute )
258 : mCaps( caps )
259 , mDefaultTypeName( defaultTypeName )
260 , mMapTableAliasToName( mapTypenameAliasToTypename )
261 , mMapTypenameToFields( mapTypenameToFields )
262 , mMapTypenameToGeometryAttribute( mapTypenameToGeometryAttribute )
263 , mError( false )
264 {
265 }
266
visit(const QgsSQLStatement::NodeColumnRef & n)267 void QgsWFSProviderSQLColumnRefValidator::visit( const QgsSQLStatement::NodeColumnRef &n )
268 {
269 if ( !mError && !n.star() )
270 {
271 QString typeName = mDefaultTypeName;
272 if ( !n.tableName().isEmpty() )
273 {
274 if ( mMapTableAliasToName.contains( n.tableName() ) )
275 typeName = mMapTableAliasToName[n.tableName()];
276 else if ( mMapTableAliasToName.contains( mCaps.addPrefixIfNeeded( n.tableName() ) ) )
277 typeName = mMapTableAliasToName[mCaps.addPrefixIfNeeded( n.tableName() )];
278 else
279 {
280 mError = true;
281 mErrorMessage = QObject::tr( "Column '%1' references a non existing table" ).
282 arg( n.dump() );
283 return;
284 }
285 }
286
287 QgsFields tableFields = mMapTypenameToFields[typeName];
288 int idx = tableFields.lookupField( n.name() );
289 if ( idx < 0 && mMapTypenameToGeometryAttribute[typeName] != n.name() )
290 {
291 mError = true;
292 mErrorMessage = QObject::tr( "Column '%1' references a non existing field" ).
293 arg( n.dump() );
294 return;
295 }
296
297 QgsSQLStatement::RecursiveVisitor::visit( n );
298 }
299 }
300
301
processSQL(const QString & sqlString,QString & errorMsg,QString & warningMsg)302 bool QgsWFSProvider::processSQL( const QString &sqlString, QString &errorMsg, QString &warningMsg )
303 {
304 QgsDebugMsgLevel( QStringLiteral( "Processing SQL: %1" ).arg( sqlString ), 4 );
305 errorMsg.clear();
306 warningMsg.clear();
307 QgsSQLStatement sql( sqlString );
308 if ( sql.hasParserError() )
309 {
310 QString parserErrorString( sql.parserErrorString() );
311 QStringList parts( parserErrorString.split( ',' ) );
312 parserErrorString.clear();
313 const auto constParts = parts;
314 for ( const QString &part : constParts )
315 {
316 QString newPart( part );
317 if ( part == QLatin1String( "syntax error" ) )
318 newPart = tr( "Syntax error." );
319 else if ( part == QLatin1String( " unexpected $end" ) )
320 newPart = tr( "Missing content at end of string." );
321 else if ( part.startsWith( QLatin1String( " unexpected " ) ) )
322 newPart = tr( "%1 is unexpected." ).arg( part.mid( QStringLiteral( " unexpected " ).size() ) );
323 else if ( part.startsWith( QLatin1String( " expecting " ) ) )
324 newPart = tr( "%1 is expected instead." ).arg( part.mid( QStringLiteral( " expecting " ).size() ) );
325 if ( !parserErrorString.isEmpty() )
326 parserErrorString += QLatin1Char( ' ' );
327 parserErrorString += newPart;
328 }
329 parserErrorString.replace( QLatin1String( " or " ), tr( "%1 or %2" ).arg( QString(), QString() ) );
330 parserErrorString.replace( QLatin1String( "COMMA" ), tr( "comma" ) );
331 parserErrorString.replace( QLatin1String( "IDENTIFIER" ), tr( "an identifier" ) );
332 errorMsg = tr( "SQL query is invalid: %1" ).arg( parserErrorString );
333 return false;
334 }
335 if ( !sql.doBasicValidationChecks( errorMsg ) )
336 {
337 errorMsg = tr( "SQL query is invalid: %1" ).arg( errorMsg );
338 return false;
339 }
340 if ( sql.rootNode()->nodeType() != QgsSQLStatement::ntSelect )
341 {
342 // Shouldn't happen
343 QgsDebugMsg( QStringLiteral( "SQL statement is not a SELECT. This should not happen" ) );
344 return false;
345 }
346 const QgsSQLStatement::NodeSelect *select = dynamic_cast<const QgsSQLStatement::NodeSelect *>( sql.rootNode() );
347 if ( !select )
348 {
349 // Makes Coverity happy, but cannot happen in practice
350 QgsDebugMsg( QStringLiteral( "should not happen" ) );
351 return false;
352 }
353 mShared->mDistinctSelect = select->distinct();
354
355 QMap< QString, QString > mapTypenameAliasToTypename;
356 QMap< QString, QString > mapTypenameToTypenameAlias; // reverse map of the previous one
357 QList<QgsSQLStatement::NodeTableDef *> tables = select->tables();
358 QList< QString> typenameList;
359 bool severalTablesWithSameNameButDifferentPrefix = false;
360 QSet< QString > unprefixTypenames;
361 const auto constTables = tables;
362 for ( QgsSQLStatement::NodeTableDef *table : constTables )
363 {
364 QString prefixedTypename( mShared->mCaps.addPrefixIfNeeded( table->name() ) );
365 if ( prefixedTypename.isEmpty() )
366 {
367 if ( mShared->mCaps.setAmbiguousUnprefixedTypename.contains( table->name() ) )
368 {
369 errorMsg = tr( "Typename '%1' is ambiguous without prefix" ).arg( table->name() );
370 }
371 else
372 {
373 errorMsg = tr( "Typename '%1' is unknown" ).arg( table->name() );
374 }
375 return false;
376 }
377 typenameList << prefixedTypename;
378 if ( unprefixTypenames.contains( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) ) )
379 {
380 severalTablesWithSameNameButDifferentPrefix = true;
381 }
382 unprefixTypenames.insert( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) );
383 if ( table->alias().isEmpty() )
384 {
385 mapTypenameAliasToTypename[ prefixedTypename ] = prefixedTypename;
386 mapTypenameToTypenameAlias[ prefixedTypename ] = prefixedTypename;
387 }
388 else
389 {
390 mapTypenameAliasToTypename[ table->alias()] = prefixedTypename;
391 mapTypenameToTypenameAlias[ prefixedTypename ] = table->alias();
392 }
393 }
394
395 QList<QgsSQLStatement::NodeJoin *> joins = select->joins();
396 const auto constJoins = joins;
397 for ( QgsSQLStatement::NodeJoin *join : constJoins )
398 {
399 QgsSQLStatement::NodeTableDef *table = join->tableDef();
400 QString prefixedTypename( mShared->mCaps.addPrefixIfNeeded( table->name() ) );
401 if ( prefixedTypename.isEmpty() )
402 {
403 if ( mShared->mCaps.setAmbiguousUnprefixedTypename.contains( table->name() ) )
404 {
405 errorMsg = tr( "Typename '%1' is ambiguous without prefix" ).arg( table->name() );
406 }
407 else
408 {
409 errorMsg = tr( "Typename '%1' is unknown" ).arg( table->name() );
410 }
411 return false;
412 }
413 typenameList << prefixedTypename;
414 if ( unprefixTypenames.contains( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) ) )
415 {
416 severalTablesWithSameNameButDifferentPrefix = true;
417 }
418 unprefixTypenames.insert( QgsWFSUtils::removeNamespacePrefix( prefixedTypename ) );
419 if ( table->alias().isEmpty() )
420 {
421 mapTypenameAliasToTypename[ prefixedTypename ] = prefixedTypename;
422 mapTypenameToTypenameAlias[ prefixedTypename ] = prefixedTypename;
423 }
424 else
425 {
426 mapTypenameAliasToTypename[ table->alias()] = prefixedTypename;
427 mapTypenameToTypenameAlias[ prefixedTypename ] = table->alias();
428 }
429 }
430
431 if ( typenameList.size() > 1 && !mShared->mCaps.supportsJoins )
432 {
433 errorMsg = tr( "JOINs are not supported by this server" );
434 return false;
435 }
436
437 if ( !typenameList.contains( mShared->mURI.typeName() ) )
438 {
439 errorMsg = tr( "FROM or JOIN clause should contain the table name '%1'" ).arg( mShared->mURI.typeName() );
440 return false;
441 }
442
443 QString concatenatedTypenames;
444 for ( const QString &typeName : qgis::as_const( typenameList ) )
445 {
446 if ( !concatenatedTypenames.isEmpty() )
447 concatenatedTypenames += QLatin1Char( ',' );
448 concatenatedTypenames += typeName;
449 }
450
451 QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
452 if ( !describeFeatureType.requestFeatureType( mShared->mWFSVersion,
453 concatenatedTypenames, mShared->mCaps ) )
454 {
455 errorMsg = tr( "DescribeFeatureType failed for url %1: %2" ).
456 arg( dataSourceUri(), describeFeatureType.errorMessage() );
457 return false;
458 }
459
460 QByteArray response = describeFeatureType.response();
461
462
463 QDomDocument describeFeatureDocument;
464 errorMsg.clear();
465 if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
466 {
467 QgsDebugMsgLevel( response, 4 );
468 errorMsg = tr( "DescribeFeatureType failed for url %1: %2" ).
469 arg( dataSourceUri(), errorMsg );
470 return false;
471 }
472
473 mShared->mLayerPropertiesList.clear();
474 QMap < QString, QgsFields > mapTypenameToFields;
475 QMap < QString, QString > mapTypenameToGeometryAttribute;
476 for ( const QString &typeName : qgis::as_const( typenameList ) )
477 {
478 QString geometryAttribute;
479 QgsFields fields;
480 QgsWkbTypes::Type geomType;
481 if ( !readAttributesFromSchema( describeFeatureDocument,
482 typeName,
483 geometryAttribute, fields, geomType, errorMsg ) )
484 {
485 errorMsg = tr( "Analysis of DescribeFeatureType response failed for url %1, typeName %2: %3" ).
486 arg( dataSourceUri(), typeName, errorMsg );
487 return false;
488 }
489
490 mapTypenameToFields[typeName] = fields;
491 mapTypenameToGeometryAttribute[typeName] = geometryAttribute;
492 if ( typeName == mShared->mURI.typeName() )
493 {
494 mShared->mGeometryAttribute = geometryAttribute;
495 mShared->mWKBType = geomType;
496 mThisTypenameFields = fields;
497 }
498
499 QgsOgcUtils::LayerProperties layerProperties;
500 layerProperties.mName = typeName;
501 layerProperties.mGeometryAttribute = geometryAttribute;
502 if ( typeName == mShared->mURI.typeName() )
503 layerProperties.mSRSName = mShared->srsName();
504
505 if ( typeName.contains( ':' ) )
506 {
507 layerProperties.mNamespaceURI = mShared->mCaps.getNamespaceForTypename( typeName );
508 layerProperties.mNamespacePrefix = QgsWFSUtils::nameSpacePrefix( typeName );
509 }
510
511 mShared->mLayerPropertiesList << layerProperties;
512 }
513
514 const QString &defaultTypeName = mShared->mURI.typeName();
515 QgsWFSProviderSQLColumnRefValidator oColumnValidator(
516 mShared->mCaps,
517 defaultTypeName,
518 mapTypenameAliasToTypename,
519 mapTypenameToFields,
520 mapTypenameToGeometryAttribute );
521 sql.acceptVisitor( oColumnValidator );
522 if ( oColumnValidator.hasError() )
523 {
524 errorMsg = oColumnValidator.errorMessage();
525 return false;
526 }
527
528 if ( mShared->mURI.validateSqlFunctions() )
529 {
530 QgsWFSProviderSQLFunctionValidator oValidator( mShared->mCaps.spatialPredicatesList,
531 mShared->mCaps.functionList );
532 sql.acceptVisitor( oValidator );
533 if ( oValidator.hasError() )
534 {
535 errorMsg = oValidator.errorMessage();
536 return false;
537 }
538 }
539
540 QList<QgsSQLStatement::NodeSelectedColumn *> columns = select->columns();
541 QMap< QString, QPair<QString, QString> > mapFieldNameToSrcLayerNameFieldName;
542 mShared->mFields.clear();
543 const auto constColumns = columns;
544 for ( QgsSQLStatement::NodeSelectedColumn *selectedcolumn : constColumns )
545 {
546 QgsSQLStatement::Node *column = selectedcolumn->column();
547 if ( column->nodeType() != QgsSQLStatement::ntColumnRef )
548 {
549 errorMsg = tr( "Column '%1' is not a direct reference to a table column." ).arg( column->dump() );
550 return false;
551 }
552 QgsSQLStatement::NodeColumnRef *columnRef = dynamic_cast<QgsSQLStatement::NodeColumnRef *>( column );
553 Q_ASSERT( columnRef );
554
555 QString columnTableTypename = defaultTypeName;
556 if ( !columnRef->tableName().isEmpty() )
557 {
558 if ( mapTypenameAliasToTypename.contains( columnRef->tableName() ) )
559 columnTableTypename = mapTypenameAliasToTypename[columnRef->tableName()];
560 else
561 columnTableTypename = mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() );
562 }
563
564 if ( columnRef->star() )
565 {
566 // table.* syntax
567 if ( !columnRef->tableName().isEmpty() )
568 {
569 const QgsFields tableFields = mapTypenameToFields[columnTableTypename];
570 for ( int i = 0; i < tableFields.size(); i++ )
571 {
572 QgsField srcField = tableFields.at( i );
573 QString fieldName( srcField.name() );
574 // If several tables selected, prefix by table name
575 if ( typenameList.size() > 1 )
576 {
577 QString tablePrefix( mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() ) );
578 if ( tablePrefix.isEmpty() ) // might be an alias
579 tablePrefix = columnRef->tableName();
580 if ( !severalTablesWithSameNameButDifferentPrefix )
581 tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
582 fieldName = tablePrefix + "." + fieldName;
583 }
584 QgsField field( srcField );
585 field.setName( fieldName );
586 if ( mapFieldNameToSrcLayerNameFieldName.contains( fieldName ) )
587 {
588 errorMsg = tr( "Field '%1': a field with the same name already exists." ).arg( field.name() );
589 return false;
590 }
591
592 mapFieldNameToSrcLayerNameFieldName[ field.name()] =
593 QPair<QString, QString>( columnTableTypename, srcField.name() );
594 mShared->mFields.append( field );
595 }
596 }
597 else
598 {
599 // * syntax
600 const auto constTypenameList = typenameList;
601 for ( const QString &typeName : constTypenameList )
602 {
603 const QgsFields tableFields = mapTypenameToFields[typeName];
604 for ( int i = 0; i < tableFields.size(); i++ )
605 {
606 QgsField srcField = tableFields.at( i );
607 QString fieldName( srcField.name() );
608 // If several tables selected, prefix by table name
609 if ( typenameList.size() > 1 )
610 {
611 QString tablePrefix( mapTypenameToTypenameAlias[typeName] );
612 if ( !severalTablesWithSameNameButDifferentPrefix )
613 tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
614 fieldName = tablePrefix + "." + fieldName;
615 }
616 QgsField field( srcField );
617 field.setName( fieldName );
618 mapFieldNameToSrcLayerNameFieldName[ field.name()] =
619 QPair<QString, QString>( typeName, srcField.name() );
620 mShared->mFields.append( field );
621 }
622 }
623 }
624 }
625 // Geometry field
626 else if ( mapTypenameToGeometryAttribute[columnTableTypename] == columnRef->name() )
627 {
628 if ( columnTableTypename != mShared->mURI.typeName() )
629 {
630 warningMsg = tr( "The geometry field of a typename that is not the main typename is ignored in the selected fields." );
631 QgsDebugMsg( warningMsg );
632 }
633 }
634 // Regular field
635 else
636 {
637 const QgsFields tableFields = mapTypenameToFields[columnTableTypename];
638 int idx = tableFields.lookupField( columnRef->name() );
639 if ( idx < 0 )
640 {
641 QgsDebugMsg( QStringLiteral( "Should not happen. Cannot find field for %1" ).arg( columnRef->name() ) );
642 continue;
643 }
644
645 QString fieldName( columnRef->name() );
646 if ( !selectedcolumn->alias().isEmpty() )
647 fieldName = selectedcolumn->alias();
648 else if ( !columnRef->tableName().isEmpty() )
649 {
650 QString tablePrefix( mShared->mCaps.addPrefixIfNeeded( columnRef->tableName() ) );
651 if ( tablePrefix.isEmpty() ) // might be an alias
652 tablePrefix = columnRef->tableName();
653 if ( !severalTablesWithSameNameButDifferentPrefix )
654 tablePrefix = QgsWFSUtils::removeNamespacePrefix( tablePrefix );
655 fieldName = tablePrefix + "." + fieldName;
656 }
657 if ( mapFieldNameToSrcLayerNameFieldName.contains( fieldName ) )
658 {
659 errorMsg = tr( "Field '%1': a field with the same name already exists." ).arg( column->dump() );
660 return false;
661 }
662
663 QgsField orig = tableFields.at( idx );
664 QgsField field( orig );
665 field.setName( fieldName );
666 mapFieldNameToSrcLayerNameFieldName[ field.name()] =
667 QPair<QString, QString>( columnTableTypename, orig.name() );
668 mShared->mFields.append( field );
669 }
670 }
671
672 mShared->mMapFieldNameToSrcLayerNameFieldName = mapFieldNameToSrcLayerNameFieldName;
673
674 return true;
675 }
676
pushErrorSlot(const QString & errorMsg)677 void QgsWFSProvider::pushErrorSlot( const QString &errorMsg )
678 {
679 pushError( errorMsg );
680 }
681
featureReceivedAnalyzeOneFeature(QVector<QgsFeatureUniqueIdPair> list)682 void QgsWFSProvider::featureReceivedAnalyzeOneFeature( QVector<QgsFeatureUniqueIdPair> list )
683 {
684 if ( list.size() != 0 )
685 {
686 QgsFeature feat = list[0].first;
687 QgsGeometry geometry = feat.geometry();
688 if ( !geometry.isNull() )
689 {
690 mShared->mWKBType = geometry.wkbType();
691
692 // Fragile heuristics that helps for
693 // https://sampleservices.luciad.com/ogc/wfs/sampleswfs?REQUEST=GetFeature&SERVICE=WFS&TYPENAME=rivers&VERSION=1.1.0
694 // In case the geometry is a geometry collection, analyze its members to
695 // see if they are of the same type. If then, assume that all features
696 // will be similar, and report the proper MultiPoint/MultiLineString/
697 // MultiPolygon type instead.
698 if ( mShared->mWKBType == QgsWkbTypes::GeometryCollection )
699 {
700 auto geomColl = geometry.asGeometryCollection();
701 mShared->mWKBType = QgsWkbTypes::Unknown;
702 for ( const auto &geom : geomColl )
703 {
704 if ( mShared->mWKBType == QgsWkbTypes::Unknown )
705 {
706 mShared->mWKBType = geom.wkbType();
707 }
708 else if ( mShared->mWKBType != geom.wkbType() )
709 {
710 mShared->mWKBType = QgsWkbTypes::Unknown;
711 break;
712 }
713 }
714 if ( mShared->mWKBType != QgsWkbTypes::Unknown )
715 {
716 if ( mShared->mWKBType == QgsWkbTypes::Point )
717 {
718 QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of Point. Advertizing optimistically as MultiPoint" ) );
719 mShared->mWKBType = QgsWkbTypes::MultiPoint;
720 }
721 else if ( mShared->mWKBType == QgsWkbTypes::LineString )
722 {
723 QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of LineString. Advertizing optimistically as MultiLineString" ) );
724 mShared->mWKBType = QgsWkbTypes::MultiLineString;
725 }
726 else if ( mShared->mWKBType == QgsWkbTypes::Polygon )
727 {
728 QgsDebugMsg( QStringLiteral( "Layer of unknown type. First element is a GeometryCollection of Polygon. Advertizing optimistically as MultiPolygon" ) );
729 mShared->mWKBType = QgsWkbTypes::MultiPolygon;
730 }
731 else
732 {
733 mShared->mWKBType = QgsWkbTypes::Unknown;
734 }
735 }
736 }
737 }
738 }
739 }
740
subsetString() const741 QString QgsWFSProvider::subsetString() const
742 {
743 return mSubsetString;
744 }
745
setSubsetString(const QString & theSQL,bool updateFeatureCount)746 bool QgsWFSProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount )
747 {
748 Q_UNUSED( updateFeatureCount )
749
750 QgsDebugMsgLevel( QStringLiteral( "theSql = '%1'" ).arg( theSQL ), 4 );
751
752 if ( theSQL == mSubsetString )
753 return true;
754
755 // Invalid and cancel current download before altering fields, etc...
756 // (crashes might happen if not done at the beginning)
757 mShared->invalidateCache();
758
759 mSubsetString = theSQL;
760 clearMinMaxCache();
761
762 // update URI
763 mShared->mFields = mThisTypenameFields;
764 mShared->mLayerPropertiesList.clear();
765 mShared->mMapFieldNameToSrcLayerNameFieldName.clear();
766 mShared->mDistinctSelect = false;
767 if ( theSQL.startsWith( QLatin1String( "SELECT " ), Qt::CaseInsensitive ) ||
768 theSQL.startsWith( QLatin1String( "SELECT\t" ), Qt::CaseInsensitive ) ||
769 theSQL.startsWith( QLatin1String( "SELECT\r" ), Qt::CaseInsensitive ) ||
770 theSQL.startsWith( QLatin1String( "SELECT\n" ), Qt::CaseInsensitive ) )
771 {
772 QString errorMsg, warningMsg;
773 if ( !processSQL( theSQL, errorMsg, warningMsg ) )
774 {
775 QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
776 return false;
777 }
778 mShared->mURI.setSql( theSQL );
779 mShared->mURI.setFilter( QString() );
780 }
781 else
782 {
783 mShared->mURI.setSql( QString() );
784 mShared->mURI.setFilter( theSQL );
785 }
786
787 setDataSourceUri( mShared->mURI.uri() );
788 QString errorMsg;
789 if ( !mShared->computeFilter( errorMsg ) )
790 QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
791
792 reloadData();
793
794 return true;
795 }
796
797
featureSource() const798 QgsAbstractFeatureSource *QgsWFSProvider::featureSource() const
799 {
800 auto fs = new QgsBackgroundCachedFeatureSource( mShared );
801 /*connect( fs, SIGNAL( extentRequested( const QgsRectangle & ) ),
802 this, SLOT( extendExtent( const QgsRectangle & ) ) );*/
803 return fs;
804 }
805
reloadProviderData()806 void QgsWFSProvider::reloadProviderData()
807 {
808 mShared->invalidateCache();
809 }
810
geometryElement(const QgsGeometry & geometry,QDomDocument & transactionDoc)811 QDomElement QgsWFSProvider::geometryElement( const QgsGeometry &geometry, QDomDocument &transactionDoc )
812 {
813 QDomElement gmlElem;
814
815 // Determine axis orientation and gml version
816 bool applyAxisInversion;
817 QgsOgcUtils::GMLVersion gmlVersion;
818
819 if ( mShared->mWFSVersion.startsWith( QLatin1String( "1.1" ) ) )
820 {
821 // WFS 1.1.0 uses preferably GML 3, but ESRI mapserver in 2020 doesn't like it so we stick to GML2
822 if ( ! mShared->mServerPrefersCoordinatesForTransactions_1_1 )
823 {
824 gmlVersion = QgsOgcUtils::GML_3_1_0;
825 }
826 else
827 {
828 gmlVersion = QgsOgcUtils::GML_2_1_2;
829 }
830 // For servers like Geomedia and QGIS Server that advertise EPSG:XXXX in capabilities even in WFS 1.1 or 2.0
831 // cpabilities useEPSGColumnFormat is set.
832 // We follow GeoServer convention here which is to treat EPSG:4326 as lon/lat
833 applyAxisInversion = ( crs().hasAxisInverted() && ! mShared->mURI.ignoreAxisOrientation() && ! mShared->mCaps.useEPSGColumnFormat )
834 || mShared->mURI.invertAxisOrientation();
835 }
836 else // 1.0
837 {
838 gmlVersion = QgsOgcUtils::GML_2_1_2;
839 applyAxisInversion = mShared->mURI.invertAxisOrientation();
840 }
841
842 gmlElem = QgsOgcUtils::geometryToGML(
843 geometry,
844 transactionDoc,
845 gmlVersion,
846 mShared->srsName(),
847 applyAxisInversion,
848 QString()
849 );
850
851 return gmlElem;
852 }
853
wkbType() const854 QgsWkbTypes::Type QgsWFSProvider::wkbType() const
855 {
856 return mShared->mWKBType;
857 }
858
featureCount() const859 long QgsWFSProvider::featureCount() const
860 {
861 return mShared->getFeatureCount();
862 }
863
fields() const864 QgsFields QgsWFSProvider::fields() const
865 {
866 return mShared->mFields;
867 }
868
geometryAttribute() const869 QString QgsWFSProvider::geometryAttribute() const
870 {
871 return mShared->mGeometryAttribute;
872 }
873
crs() const874 QgsCoordinateReferenceSystem QgsWFSProvider::crs() const
875 {
876 return mShared->mSourceCrs;
877 }
878
extent() const879 QgsRectangle QgsWFSProvider::extent() const
880 {
881 return mShared->consolidatedExtent();
882 }
883
isValid() const884 bool QgsWFSProvider::isValid() const
885 {
886 return mValid;
887 }
888
getFeatures(const QgsFeatureRequest & request) const889 QgsFeatureIterator QgsWFSProvider::getFeatures( const QgsFeatureRequest &request ) const
890 {
891 return QgsFeatureIterator( new QgsBackgroundCachedFeatureIterator( new QgsBackgroundCachedFeatureSource( mShared ), true, mShared, request ) );
892 }
893
addFeatures(QgsFeatureList & flist,Flags flags)894 bool QgsWFSProvider::addFeatures( QgsFeatureList &flist, Flags flags )
895 {
896 //create <Transaction> xml
897 QDomDocument transactionDoc;
898 QDomElement transactionElem = createTransactionElement( transactionDoc );
899 transactionDoc.appendChild( transactionElem );
900
901 //find out typename from uri and strip namespace prefix
902 QString tname = mShared->mURI.typeName();
903 if ( tname.isNull() )
904 {
905 return false;
906 }
907 tname = QgsWFSUtils::removeNamespacePrefix( tname );
908
909 //Add the features
910 QgsFeatureList::iterator featureIt = flist.begin();
911 for ( ; featureIt != flist.end(); ++featureIt )
912 {
913 //Insert element
914 QDomElement insertElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Insert" ) );
915 transactionElem.appendChild( insertElem );
916
917 QDomElement featureElem = transactionDoc.createElementNS( mApplicationNamespace, tname );
918
919 QgsAttributes featureAttributes = featureIt->attributes();
920 int nAttrs = featureAttributes.size();
921 for ( int i = 0; i < nAttrs; ++i )
922 {
923 const QVariant &value = featureAttributes.at( i );
924 if ( value.isValid() && !value.isNull() )
925 {
926 QDomElement fieldElem = transactionDoc.createElementNS( mApplicationNamespace, mShared->mFields.at( i ).name() );
927 QDomText fieldText = transactionDoc.createTextNode( convertToXML( value ) );
928 fieldElem.appendChild( fieldText );
929 featureElem.appendChild( fieldElem );
930 }
931 }
932
933 //add geometry column (as gml)
934 QgsGeometry geometry = featureIt->geometry();
935 if ( !geometry.isNull() )
936 {
937 QDomElement geomElem = transactionDoc.createElementNS( mApplicationNamespace, mShared->mGeometryAttribute );
938 QgsGeometry the_geom( geometry );
939 // convert to multi if the layer geom type is multi and the geom is not
940 if ( QgsWkbTypes::isMultiType( this->wkbType() ) && ! the_geom.isMultipart() )
941 {
942 the_geom.convertToMultiType();
943 }
944
945 const QDomElement gmlElem { geometryElement( the_geom, transactionDoc ) };
946 if ( ! gmlElem.isNull() )
947 {
948 geomElem.appendChild( gmlElem );
949 featureElem.appendChild( geomElem );
950 }
951 }
952
953 insertElem.appendChild( featureElem );
954 }
955
956 QDomDocument serverResponse;
957 bool success = sendTransactionDocument( transactionDoc, serverResponse );
958 if ( !success )
959 {
960 return false;
961 }
962
963 if ( transactionSuccess( serverResponse ) )
964 {
965 //transaction successful. Add the features to the cache
966 QStringList idList = insertedFeatureIds( serverResponse );
967 /* Fix issue with GeoServer and shapefile feature stores when no real
968 feature id are returned but new0 returned instead of the featureId*/
969 const auto constIdList = idList;
970 for ( const QString &v : constIdList )
971 {
972 if ( v.startsWith( QLatin1String( "new" ) ) )
973 {
974 reloadData();
975 return true;
976 }
977 }
978 QStringList::const_iterator idIt = idList.constBegin();
979 featureIt = flist.begin();
980
981 QVector<QgsFeatureUniqueIdPair> serializedFeatureList;
982 for ( ; idIt != idList.constEnd() && featureIt != flist.end(); ++idIt, ++featureIt )
983 {
984 serializedFeatureList.push_back( QgsFeatureUniqueIdPair( *featureIt, *idIt ) );
985 }
986 mShared->serializeFeatures( serializedFeatureList );
987
988 if ( !( flags & QgsFeatureSink::FastInsert ) )
989 {
990 // And now set the feature id from the one got from the database
991 QMap< QString, QgsFeatureId > map;
992 for ( int idx = 0; idx < serializedFeatureList.size(); idx++ )
993 map[ serializedFeatureList[idx].second ] = serializedFeatureList[idx].first.id();
994
995 idIt = idList.constBegin();
996 featureIt = flist.begin();
997 for ( ; idIt != idList.constEnd() && featureIt != flist.end(); ++idIt, ++featureIt )
998 {
999 if ( map.find( *idIt ) != map.end() )
1000 featureIt->setId( map[*idIt] );
1001 }
1002 }
1003
1004 return true;
1005 }
1006 else
1007 {
1008 handleException( serverResponse );
1009 return false;
1010 }
1011 }
1012
deleteFeatures(const QgsFeatureIds & id)1013 bool QgsWFSProvider::deleteFeatures( const QgsFeatureIds &id )
1014 {
1015 if ( id.size() < 1 )
1016 {
1017 return true;
1018 }
1019
1020 //find out typename from uri and strip namespace prefix
1021 QString tname = mShared->mURI.typeName();
1022 if ( tname.isNull() )
1023 {
1024 return false;
1025 }
1026
1027 //create <Transaction> xml
1028 QDomDocument transactionDoc;
1029 QDomElement transactionElem = createTransactionElement( transactionDoc );
1030 transactionDoc.appendChild( transactionElem );
1031 //delete element
1032 QDomElement deleteElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Delete" ) );
1033 deleteElem.setAttribute( QStringLiteral( "typeName" ), tname );
1034 QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1035
1036
1037 QgsFeatureIds::const_iterator idIt = id.constBegin();
1038 for ( ; idIt != id.constEnd(); ++idIt )
1039 {
1040 //find out feature id
1041 QString gmlid = mShared->findUniqueId( *idIt );
1042 if ( gmlid.isEmpty() )
1043 {
1044 QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( *idIt ) );
1045 continue;
1046 }
1047 QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1048 featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1049 filterElem.appendChild( featureIdElem );
1050 }
1051
1052 deleteElem.appendChild( filterElem );
1053 transactionElem.appendChild( deleteElem );
1054
1055 QDomDocument serverResponse;
1056 bool success = sendTransactionDocument( transactionDoc, serverResponse );
1057 if ( !success )
1058 {
1059 return false;
1060 }
1061
1062 if ( transactionSuccess( serverResponse ) )
1063 {
1064 mShared->deleteFeatures( id );
1065 return true;
1066 }
1067 else
1068 {
1069 handleException( serverResponse );
1070 return false;
1071 }
1072 }
1073
changeGeometryValues(const QgsGeometryMap & geometry_map)1074 bool QgsWFSProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
1075 {
1076 //find out typename from uri
1077 QString tname = mShared->mURI.typeName();
1078 if ( tname.isNull() )
1079 {
1080 return false;
1081 }
1082
1083 QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( tname );
1084 if ( !namespacePrefix.isEmpty() )
1085 {
1086 namespacePrefix += ':';
1087 }
1088
1089 //create <Transaction> xml
1090 QDomDocument transactionDoc;
1091 QDomElement transactionElem = createTransactionElement( transactionDoc );
1092 transactionDoc.appendChild( transactionElem );
1093
1094 QgsGeometryMap::const_iterator geomIt = geometry_map.constBegin();
1095 for ( ; geomIt != geometry_map.constEnd(); ++geomIt )
1096 {
1097 QString gmlid = mShared->findUniqueId( geomIt.key() );
1098 if ( gmlid.isEmpty() )
1099 {
1100 QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( geomIt.key() ) );
1101 continue;
1102 }
1103 QDomElement updateElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Update" ) );
1104 updateElem.setAttribute( QStringLiteral( "typeName" ), tname );
1105 //Property
1106 QDomElement propertyElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Property" ) );
1107 QDomElement nameElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Name" ) );
1108 QDomText nameText = transactionDoc.createTextNode( namespacePrefix + mShared->mGeometryAttribute );
1109 nameElem.appendChild( nameText );
1110 propertyElem.appendChild( nameElem );
1111 QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) );
1112
1113 valueElem.appendChild( geometryElement( geomIt.value(), transactionDoc ) );
1114
1115 propertyElem.appendChild( valueElem );
1116 updateElem.appendChild( propertyElem );
1117
1118 //filter
1119 QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1120 QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1121 featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1122 filterElem.appendChild( featureIdElem );
1123 updateElem.appendChild( filterElem );
1124
1125 transactionElem.appendChild( updateElem );
1126 }
1127
1128 QDomDocument serverResponse;
1129 bool success = sendTransactionDocument( transactionDoc, serverResponse );
1130 if ( !success )
1131 {
1132 return false;
1133 }
1134
1135 if ( transactionSuccess( serverResponse ) )
1136 {
1137 mShared->changeGeometryValues( geometry_map );
1138 return true;
1139 }
1140 else
1141 {
1142 handleException( serverResponse );
1143 return false;
1144 }
1145 }
1146
convertToXML(const QVariant & value)1147 QString QgsWFSProvider::convertToXML( const QVariant &value )
1148 {
1149 QString valueStr( value.toString() );
1150 if ( value.type() == QVariant::DateTime )
1151 {
1152 QDateTime dt = value.toDateTime().toUTC();
1153 if ( !dt.isNull() )
1154 {
1155 valueStr = dt.toString( QStringLiteral( "yyyy-MM-ddThh:mm:ss.zzzZ" ) );
1156 }
1157 else
1158 {
1159 valueStr = QString();
1160 }
1161 }
1162 return valueStr;
1163 }
1164
changeAttributeValues(const QgsChangedAttributesMap & attr_map)1165 bool QgsWFSProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
1166 {
1167 //find out typename from uri
1168 QString tname = mShared->mURI.typeName();
1169 if ( tname.isNull() )
1170 {
1171 return false;
1172 }
1173
1174 QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( tname );
1175 if ( !namespacePrefix.isEmpty() )
1176 {
1177 namespacePrefix += ':';
1178 }
1179
1180 //create <Transaction> xml
1181 QDomDocument transactionDoc;
1182 QDomElement transactionElem = createTransactionElement( transactionDoc );
1183 transactionDoc.appendChild( transactionElem );
1184
1185 QgsChangedAttributesMap::const_iterator attIt = attr_map.constBegin();
1186 for ( ; attIt != attr_map.constEnd(); ++attIt )
1187 {
1188 QString gmlid = mShared->findUniqueId( attIt.key() );
1189 if ( gmlid.isEmpty() )
1190 {
1191 QgsDebugMsg( QStringLiteral( "Cannot identify feature of id %1" ).arg( attIt.key() ) );
1192 continue;
1193 }
1194
1195 QDomElement updateElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Update" ) );
1196 updateElem.setAttribute( QStringLiteral( "typeName" ), tname );
1197
1198 QgsAttributeMap::const_iterator attMapIt = attIt.value().constBegin();
1199 for ( ; attMapIt != attIt.value().constEnd(); ++attMapIt )
1200 {
1201 QString fieldName = mShared->mFields.at( attMapIt.key() ).name();
1202 QDomElement propertyElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Property" ) );
1203
1204 QDomElement nameElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Name" ) );
1205 QDomText nameText = transactionDoc.createTextNode( namespacePrefix + fieldName );
1206 nameElem.appendChild( nameText );
1207 propertyElem.appendChild( nameElem );
1208
1209 QDomElement valueElem = transactionDoc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Value" ) );
1210
1211 if ( attMapIt.value().isValid() && !attMapIt.value().isNull() )
1212 {
1213 // WFS does not support :nil='true'
1214 // if value is NULL, do not add value element
1215 QDomText valueText = transactionDoc.createTextNode( convertToXML( attMapIt.value() ) );
1216 valueElem.appendChild( valueText );
1217 propertyElem.appendChild( valueElem );
1218 }
1219
1220 updateElem.appendChild( propertyElem );
1221 }
1222
1223 //Filter
1224 QDomElement filterElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "Filter" ) );
1225 QDomElement featureIdElem = transactionDoc.createElementNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1226 featureIdElem.setAttribute( QStringLiteral( "fid" ), gmlid );
1227 filterElem.appendChild( featureIdElem );
1228 updateElem.appendChild( filterElem );
1229
1230 transactionElem.appendChild( updateElem );
1231 }
1232
1233 QDomDocument serverResponse;
1234 bool success = sendTransactionDocument( transactionDoc, serverResponse );
1235 if ( !success )
1236 {
1237 return false;
1238 }
1239
1240 if ( transactionSuccess( serverResponse ) )
1241 {
1242 mShared->changeAttributeValues( attr_map );
1243 return true;
1244 }
1245 else
1246 {
1247 handleException( serverResponse );
1248 return false;
1249 }
1250 }
1251
metadata() const1252 QVariantMap QgsWFSProvider::metadata() const
1253 {
1254 QVariantMap result;
1255 result[QStringLiteral( "MaxFeatures" )] = mShared->mCaps.maxFeatures;
1256 result[QStringLiteral( "SupportsPaging" )] = mShared->mCaps.supportsPaging;
1257 result[QStringLiteral( "SupportsJoins" )] = mShared->mCaps.supportsJoins;
1258 return result;
1259 }
1260
translateMetadataKey(const QString & mdKey) const1261 QString QgsWFSProvider::translateMetadataKey( const QString &mdKey ) const
1262 {
1263 if ( mdKey == QLatin1String( "MaxFeatures" ) )
1264 {
1265 return tr( "Max Features" );
1266 }
1267 else if ( mdKey == QLatin1String( "SupportsPaging" ) )
1268 {
1269 return tr( "Supports Paging" );
1270 }
1271 else if ( mdKey == QLatin1String( "SupportsJoins" ) )
1272 {
1273 return tr( "Supports Joins" );
1274 }
1275 else
1276 {
1277 return mdKey;
1278 }
1279 };
1280
translateMetadataValue(const QString & mdKey,const QVariant & value) const1281 QString QgsWFSProvider::translateMetadataValue( const QString &mdKey, const QVariant &value ) const
1282 {
1283 if ( mdKey == QLatin1String( "MaxFeatures" ) )
1284 {
1285 return value.toInt() == 0 ? tr( "not provided" ) : value.toString();
1286 }
1287 else if ( mdKey == QLatin1String( "SupportsPaging" ) || mdKey == QLatin1String( "SupportsJoins" ) )
1288 {
1289 return value.toBool() ? tr( "supported" ) : tr( "unsupported" );
1290 }
1291 else
1292 {
1293 return value.toString();
1294 }
1295 }
1296
empty() const1297 bool QgsWFSProvider::empty() const
1298 {
1299 QgsFeature f;
1300 QgsFeatureRequest request;
1301 request.setNoAttributes();
1302 request.setFlags( QgsFeatureRequest::NoGeometry );
1303
1304 // Whoops, the WFS provider returns an empty iterator when we are using
1305 // a setLimit call in combination with a subsetString.
1306 // Remove this method (and default to the QgsVectorDataProvider one)
1307 // once this is fixed
1308 #if 0
1309 request.setLimit( 1 );
1310 #endif
1311 return !getFeatures( request ).nextFeature( f );
1312
1313 };
1314
describeFeatureType(QString & geometryAttribute,QgsFields & fields,QgsWkbTypes::Type & geomType)1315 bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields &fields, QgsWkbTypes::Type &geomType )
1316 {
1317 fields.clear();
1318
1319 QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
1320
1321 if ( !describeFeatureType.requestFeatureType( mShared->mWFSVersion,
1322 mShared->mURI.typeName(), mShared->mCaps ) )
1323 {
1324 QgsMessageLog::logMessage( tr( "DescribeFeatureType network request failed for url %1: %2" ).
1325 arg( dataSourceUri(), describeFeatureType.errorMessage() ), tr( "WFS" ) );
1326 return false;
1327 }
1328
1329 QByteArray response = describeFeatureType.response();
1330
1331 QgsDebugMsgLevel( response, 4 );
1332
1333 QDomDocument describeFeatureDocument;
1334 QString errorMsg;
1335 if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
1336 {
1337 QgsDebugMsgLevel( response, 4 );
1338 QgsMessageLog::logMessage( tr( "DescribeFeatureType XML parse failed for url %1: %2" ).
1339 arg( dataSourceUri(), errorMsg ), tr( "WFS" ) );
1340 return false;
1341 }
1342
1343 if ( !readAttributesFromSchema( describeFeatureDocument,
1344 mShared->mURI.typeName(),
1345 geometryAttribute, fields, geomType, errorMsg ) )
1346 {
1347 QgsDebugMsgLevel( response, 4 );
1348 QgsMessageLog::logMessage( tr( "Analysis of DescribeFeatureType response failed for url %1: %2" ).
1349 arg( dataSourceUri(), errorMsg ), tr( "WFS" ) );
1350 return false;
1351 }
1352
1353 return true;
1354 }
1355
readAttributesFromSchema(QDomDocument & schemaDoc,const QString & prefixedTypename,QString & geometryAttribute,QgsFields & fields,QgsWkbTypes::Type & geomType,QString & errorMsg)1356 bool QgsWFSProvider::readAttributesFromSchema( QDomDocument &schemaDoc,
1357 const QString &prefixedTypename,
1358 QString &geometryAttribute,
1359 QgsFields &fields,
1360 QgsWkbTypes::Type &geomType,
1361 QString &errorMsg )
1362 {
1363 //get the <schema> root element
1364 QDomNodeList schemaNodeList = schemaDoc.elementsByTagNameNS( QgsWFSConstants::XMLSCHEMA_NAMESPACE, QStringLiteral( "schema" ) );
1365 if ( schemaNodeList.length() < 1 )
1366 {
1367 errorMsg = tr( "Cannot find schema root element" );
1368 return false;
1369 }
1370 QDomElement schemaElement = schemaNodeList.at( 0 ).toElement();
1371 mApplicationNamespace = schemaElement.attribute( QStringLiteral( "targetNamespace" ) );
1372
1373 // Remove the namespace on the typename
1374 QString unprefixedTypename = prefixedTypename;
1375 if ( unprefixedTypename.contains( ':' ) )
1376 {
1377 unprefixedTypename = unprefixedTypename.section( ':', 1 );
1378 }
1379
1380 // Find the element whose name is the typename that interests us, and
1381 // collect the correspond type.
1382 QDomElement elementElement = schemaElement.firstChildElement( QStringLiteral( "element" ) );
1383 QString elementTypeString;
1384 QDomElement complexTypeElement;
1385 while ( !elementElement.isNull() )
1386 {
1387 QString name = elementElement.attribute( QStringLiteral( "name" ) );
1388 if ( name == unprefixedTypename )
1389 {
1390 elementTypeString = elementElement.attribute( QStringLiteral( "type" ) );
1391 if ( elementTypeString.isEmpty() )
1392 {
1393 // e.g http://afnemers.ruimtelijkeplannen.nl/afnemers2012/services?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=app:Bouwvlak
1394 complexTypeElement = elementElement.firstChildElement( QStringLiteral( "complexType" ) );
1395 }
1396 break;
1397 }
1398 elementElement = elementElement.nextSiblingElement( QStringLiteral( "element" ) );
1399 }
1400 // Try to get a complex type whose name contains the unprefixed typename
1401 if ( elementTypeString.isEmpty() && complexTypeElement.isNull() )
1402 {
1403 const QDomNodeList complexElements = schemaElement.elementsByTagName( QStringLiteral( "complexType" ) );
1404 for ( int i = 0; i < complexElements.size(); i++ )
1405 {
1406 if ( complexElements.at( i ).toElement().attribute( QStringLiteral( "name" ) ).contains( unprefixedTypename ) )
1407 {
1408 complexTypeElement = complexElements.at( i ).toElement();
1409 break;
1410 }
1411 }
1412 }
1413 // Give up :(
1414 if ( elementTypeString.isEmpty() && complexTypeElement.isNull() )
1415 {
1416 // "http://demo.deegree.org/inspire-workspace/services/wfs?SERVICE=WFS&REQUEST=DescribeFeatureType&VERSION=2.0.0&TYPENAME=ad:Address"
1417 QDomElement iter = schemaElement.firstChildElement();
1418 bool onlyIncludeOrImport = true;
1419 bool foundImport = false;
1420 int countInclude = 0;
1421 QDomElement includeElement;
1422 while ( !iter.isNull() )
1423 {
1424 if ( iter.tagName() == QLatin1String( "import" ) )
1425 foundImport = true;
1426 else if ( iter.tagName() == QLatin1String( "include" ) )
1427 {
1428 countInclude++;
1429 if ( countInclude == 1 )
1430 {
1431 includeElement = iter;
1432 }
1433 }
1434 else
1435 {
1436 onlyIncludeOrImport = false;
1437 break;
1438 }
1439 iter = iter.nextSiblingElement();
1440 }
1441 if ( foundImport && onlyIncludeOrImport )
1442 {
1443 errorMsg = tr( "It is probably a schema for Complex Features." );
1444 }
1445 // e.g http://services.cuzk.cz/wfs/inspire-CP-wfs.asp?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType
1446 // which has a single <include schemaLocation="http://inspire.ec.europa.eu/schemas/cp/4.0/CadastralParcels.xsd"/>
1447 // In that case, follow the link.
1448 else if ( !foundImport && countInclude == 1 )
1449 {
1450 QString schemaLocation =
1451 includeElement.attribute( QStringLiteral( "schemaLocation" ) );
1452 QgsDebugMsgLevel( QStringLiteral( "DescribeFeatureType response redirects to: %1" ).arg( schemaLocation ), 4 );
1453
1454 QgsWFSDescribeFeatureType describeFeatureType( mShared->mURI );
1455 if ( !describeFeatureType.sendGET( schemaLocation, QString(), true, false ) )
1456 {
1457 errorMsg = tr( "Cannot find schema indicated in DescribeFeatureType response." );
1458 QgsMessageLog::logMessage( tr( "DescribeFeatureType network request failed for url %1: %2" ).
1459 arg( schemaLocation, describeFeatureType.errorMessage() ), tr( "WFS" ) );
1460 return false;
1461 }
1462
1463 QByteArray response = describeFeatureType.response();
1464 QDomDocument describeFeatureDocument;
1465 if ( !describeFeatureDocument.setContent( response, true, &errorMsg ) )
1466 {
1467 QgsDebugMsgLevel( response, 4 );
1468 errorMsg = tr( "DescribeFeatureType XML parse failed for url %1: %2" ).
1469 arg( schemaLocation, errorMsg );
1470 }
1471
1472 return readAttributesFromSchema( describeFeatureDocument,
1473 prefixedTypename,
1474 geometryAttribute,
1475 fields,
1476 geomType,
1477 errorMsg );
1478
1479 }
1480 else
1481 {
1482 errorMsg = tr( "Cannot find element '%1'" ).arg( unprefixedTypename );
1483 }
1484 return false;
1485 }
1486
1487 //remove the namespace on type
1488 if ( elementTypeString.contains( ':' ) )
1489 {
1490 elementTypeString = elementTypeString.section( ':', 1 );
1491 }
1492
1493 if ( complexTypeElement.isNull() )
1494 {
1495 //the <complexType> element corresponding to the feature type
1496 complexTypeElement = schemaElement.firstChildElement( QStringLiteral( "complexType" ) );
1497 while ( !complexTypeElement.isNull() )
1498 {
1499 QString name = complexTypeElement.attribute( QStringLiteral( "name" ) );
1500 if ( name == elementTypeString )
1501 {
1502 break;
1503 }
1504 complexTypeElement = complexTypeElement.nextSiblingElement( QStringLiteral( "complexType" ) );
1505 }
1506 if ( complexTypeElement.isNull() )
1507 {
1508 errorMsg = tr( "Cannot find ComplexType element '%1'" ).arg( elementTypeString );
1509 return false;
1510 }
1511 }
1512
1513 //we have the relevant <complexType> element. Now find out the geometry and the thematic attributes
1514 QDomNodeList attributeNodeList = complexTypeElement.elementsByTagNameNS( QgsWFSConstants::XMLSCHEMA_NAMESPACE, QStringLiteral( "element" ) );
1515 if ( attributeNodeList.size() < 1 )
1516 {
1517 errorMsg = tr( "Cannot find attribute elements" );
1518 return false;
1519 }
1520
1521 bool foundGeometryAttribute = false;
1522
1523 for ( int i = 0; i < attributeNodeList.size(); ++i )
1524 {
1525 QDomElement attributeElement = attributeNodeList.at( i ).toElement();
1526
1527 //attribute name
1528 QString name = attributeElement.attribute( QStringLiteral( "name" ) );
1529 // Some servers like http://ogi.state.ok.us/geoserver/wfs on layer ogi:doq_centroids
1530 // return attribute names padded with spaces. See https://github.com/qgis/QGIS/issues/13486
1531 // I'm not completely sure how legal this
1532 // is but this validates with Xerces 3.1, and its schema analyzer does also the trimming.
1533 name = name.trimmed();
1534
1535 //attribute type
1536 QString type = attributeElement.attribute( QStringLiteral( "type" ) );
1537 if ( type.isEmpty() )
1538 {
1539 QDomElement extension = attributeElement.firstChildElement( QStringLiteral( "complexType" ) ).
1540 firstChildElement( QStringLiteral( "simpleContent" ) ).firstChildElement( QStringLiteral( "extension" ) );
1541 if ( !extension.isNull() )
1542 {
1543 type = extension.attribute( QStringLiteral( "base" ) );
1544 }
1545 }
1546
1547 // attribute ref
1548 QString ref = attributeElement.attribute( QStringLiteral( "ref" ) );
1549
1550 QRegExp gmlPT( "gml:(.*)PropertyType" );
1551 QRegExp gmlRefProperty( "gml:(.*)Property" );
1552
1553 // gmgml: is Geomedia Web Server
1554 if ( ! foundGeometryAttribute && type == QLatin1String( "gmgml:Polygon_Surface_MultiSurface_CompositeSurfacePropertyType" ) )
1555 {
1556 foundGeometryAttribute = true;
1557 geometryAttribute = name;
1558 geomType = QgsWkbTypes::MultiPolygon;
1559 }
1560 else if ( ! foundGeometryAttribute && type == QLatin1String( "gmgml:LineString_Curve_MultiCurve_CompositeCurvePropertyType" ) )
1561 {
1562 foundGeometryAttribute = true;
1563 geometryAttribute = name;
1564 geomType = QgsWkbTypes::MultiLineString;
1565 }
1566 // such as http://go.geozug.ch/Zug_WFS_Baumkataster/service.svc/get
1567 else if ( type == QLatin1String( "gmgml:Point_MultiPointPropertyType" ) )
1568 {
1569 foundGeometryAttribute = true;
1570 geometryAttribute = name;
1571 geomType = QgsWkbTypes::MultiPoint;
1572 }
1573 //is it a geometry attribute?
1574 // the GeometryAssociationType has been seen in #11785
1575 else if ( ! foundGeometryAttribute && ( type.indexOf( gmlPT ) == 0 || type == QLatin1String( "gml:GeometryAssociationType" ) ) )
1576 {
1577 foundGeometryAttribute = true;
1578 geometryAttribute = name;
1579 // We have a choice parent element we cannot assume any valid information over the geometry type
1580 if ( attributeElement.parentNode().nodeName() == QLatin1String( "choice" ) && ! attributeElement.nextSibling().isNull() )
1581 geomType = QgsWkbTypes::Unknown;
1582 else
1583 geomType = geomTypeFromPropertyType( geometryAttribute, gmlPT.cap( 1 ) );
1584 }
1585 //MH 090428: sometimes the <element> tags for geometry attributes have only attribute ref="gml:polygonProperty"
1586 //Note: this was deprecated with GML3.
1587 else if ( ! foundGeometryAttribute && ref.indexOf( gmlRefProperty ) == 0 )
1588 {
1589 foundGeometryAttribute = true;
1590 geometryAttribute = ref.mid( 4 ); // Strip gml: prefix
1591 QString propertyType( gmlRefProperty.cap( 1 ) );
1592 // Set the first character in upper case
1593 propertyType = propertyType.at( 0 ).toUpper() + propertyType.mid( 1 );
1594 geomType = geomTypeFromPropertyType( geometryAttribute, propertyType );
1595 }
1596 else if ( !name.isEmpty() ) //todo: distinguish between numerical and non-numerical types
1597 {
1598 QVariant::Type attributeType = QVariant::String; //string is default type
1599 if ( type.contains( QLatin1String( "double" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "float" ), Qt::CaseInsensitive ) || type.contains( QLatin1String( "decimal" ), Qt::CaseInsensitive ) )
1600 {
1601 attributeType = QVariant::Double;
1602 }
1603 else if ( type.contains( QLatin1String( "int" ), Qt::CaseInsensitive ) ||
1604 type.contains( QLatin1String( "short" ), Qt::CaseInsensitive ) )
1605 {
1606 attributeType = QVariant::Int;
1607 }
1608 else if ( type.contains( QLatin1String( "long" ), Qt::CaseInsensitive ) )
1609 {
1610 attributeType = QVariant::LongLong;
1611 }
1612 else if ( type.contains( QLatin1String( "dateTime" ), Qt::CaseInsensitive ) )
1613 {
1614 attributeType = QVariant::DateTime;
1615 }
1616 fields.append( QgsField( name, attributeType, type ) );
1617 }
1618 }
1619 if ( !foundGeometryAttribute )
1620 {
1621 geomType = QgsWkbTypes::NoGeometry;
1622 }
1623
1624 return true;
1625 }
1626
name() const1627 QString QgsWFSProvider::name() const
1628 {
1629 return WFS_PROVIDER_KEY;
1630 }
1631
description() const1632 QString QgsWFSProvider::description() const
1633 {
1634 return WFS_PROVIDER_DESCRIPTION;
1635 }
1636
capabilities() const1637 QgsVectorDataProvider::Capabilities QgsWFSProvider::capabilities() const
1638 {
1639 return mCapabilities;
1640 }
1641
sendTransactionDocument(const QDomDocument & doc,QDomDocument & serverResponse)1642 bool QgsWFSProvider::sendTransactionDocument( const QDomDocument &doc, QDomDocument &serverResponse )
1643 {
1644 if ( doc.isNull() )
1645 {
1646 return false;
1647 }
1648
1649 QgsDebugMsgLevel( doc.toString(), 4 );
1650
1651 QgsWFSTransactionRequest request( mShared->mURI );
1652 return request.send( doc, serverResponse );
1653 }
1654
createTransactionElement(QDomDocument & doc) const1655 QDomElement QgsWFSProvider::createTransactionElement( QDomDocument &doc ) const
1656 {
1657 QDomElement transactionElem = doc.createElementNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Transaction" ) );
1658 const QString WfsVersion = mShared->mWFSVersion;
1659 // only 1.1.0 and 1.0.0 are supported
1660 if ( WfsVersion == QStringLiteral( "1.1.0" ) )
1661 {
1662 transactionElem.setAttribute( QStringLiteral( "version" ), WfsVersion );
1663 }
1664 else
1665 {
1666 transactionElem.setAttribute( QStringLiteral( "version" ), QStringLiteral( "1.0.0" ) );
1667 }
1668 transactionElem.setAttribute( QStringLiteral( "service" ), QStringLiteral( "WFS" ) );
1669 transactionElem.setAttribute( QStringLiteral( "xmlns:xsi" ), QStringLiteral( "http://www.w3.org/2001/XMLSchema-instance" ) );
1670
1671 QUrl describeFeatureTypeURL = mShared->mURI.requestUrl( QStringLiteral( "DescribeFeatureType" ) );
1672 // For tests (since the URL contains part of random data, we need to replace it with a fixed content)
1673 if ( describeFeatureTypeURL.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
1674 {
1675 describeFeatureTypeURL = QUrl( QStringLiteral( "http://fake_qgis_http_endpoint" ) );
1676 QUrlQuery query( describeFeatureTypeURL );
1677 query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) );
1678 describeFeatureTypeURL.setQuery( query );
1679 }
1680 QUrlQuery query( describeFeatureTypeURL );
1681 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) );
1682 query.addQueryItem( QStringLiteral( "TYPENAME" ), mShared->mURI.typeName() );
1683 describeFeatureTypeURL.setQuery( query );
1684
1685 transactionElem.setAttribute( QStringLiteral( "xsi:schemaLocation" ), mApplicationNamespace + ' '
1686 + describeFeatureTypeURL.toEncoded() );
1687
1688 QString namespacePrefix = QgsWFSUtils::nameSpacePrefix( mShared->mURI.typeName() );
1689 if ( !namespacePrefix.isEmpty() )
1690 {
1691 transactionElem.setAttribute( "xmlns:" + namespacePrefix, mApplicationNamespace );
1692 }
1693 transactionElem.setAttribute( QStringLiteral( "xmlns:gml" ), QgsWFSConstants::GML_NAMESPACE );
1694
1695 return transactionElem;
1696 }
1697
transactionSuccess(const QDomDocument & serverResponse) const1698 bool QgsWFSProvider::transactionSuccess( const QDomDocument &serverResponse ) const
1699 {
1700 if ( serverResponse.isNull() )
1701 {
1702 return false;
1703 }
1704
1705 QDomElement documentElem = serverResponse.documentElement();
1706 if ( documentElem.isNull() )
1707 {
1708 return false;
1709 }
1710
1711 const QString WfsVersion = mShared->mWFSVersion;
1712
1713 if ( WfsVersion == QStringLiteral( "1.1.0" ) )
1714 {
1715 const QDomNodeList transactionSummaryList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionSummary" ) );
1716 if ( transactionSummaryList.size() < 1 )
1717 {
1718 return false;
1719 }
1720
1721 QDomElement transactionElement { transactionSummaryList.at( 0 ).toElement() };
1722 QDomNodeList totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalInserted" ) );
1723 QDomNodeList totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalUpdated" ) );
1724 QDomNodeList totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "totalDeleted" ) );
1725 if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
1726 {
1727 return true;
1728 }
1729 if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
1730 {
1731 return true;
1732 }
1733 if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
1734 {
1735 return true;
1736 }
1737
1738 // Handle wrong QGIS server response (capital initial letter)
1739 totalInserted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalInserted" ) );
1740 totalUpdated = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalUpdated" ) );
1741 totalDeleted = transactionElement.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TotalDeleted" ) );
1742 if ( totalInserted.size() > 0 && totalInserted.at( 0 ).toElement().text().toInt() > 0 )
1743 {
1744 return true;
1745 }
1746 if ( totalUpdated.size() > 0 && totalUpdated.at( 0 ).toElement().text().toInt() > 0 )
1747 {
1748 return true;
1749 }
1750 if ( totalDeleted.size() > 0 && totalDeleted.at( 0 ).toElement().text().toInt() > 0 )
1751 {
1752 return true;
1753 }
1754
1755 return false;
1756
1757 }
1758 else
1759 {
1760 const QDomNodeList transactionResultList = documentElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "TransactionResult" ) );
1761 if ( transactionResultList.size() < 1 )
1762 {
1763 return false;
1764 }
1765
1766 const QDomNodeList statusList = transactionResultList.at( 0 ).toElement().elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, QStringLiteral( "Status" ) );
1767 if ( statusList.size() < 1 )
1768 {
1769 return false;
1770 }
1771
1772 return statusList.at( 0 ).firstChildElement().localName() == QLatin1String( "SUCCESS" );
1773 }
1774
1775 }
1776
insertedFeatureIds(const QDomDocument & serverResponse) const1777 QStringList QgsWFSProvider::insertedFeatureIds( const QDomDocument &serverResponse ) const
1778 {
1779 QStringList ids;
1780 if ( serverResponse.isNull() )
1781 {
1782 return ids;
1783 }
1784
1785 QDomElement rootElem = serverResponse.documentElement();
1786 if ( rootElem.isNull() )
1787 {
1788 return ids;
1789 }
1790
1791 // Handles WFS 1.1.0
1792 QString insertResultTagName;
1793 if ( mShared->mWFSVersion == QStringLiteral( "1.1.0" ) )
1794 {
1795 insertResultTagName = QStringLiteral( "InsertResults" );
1796 }
1797 else
1798 {
1799 insertResultTagName = QStringLiteral( "InsertResult" );
1800 }
1801 QDomNodeList insertResultList = rootElem.elementsByTagNameNS( QgsWFSConstants::WFS_NAMESPACE, insertResultTagName );
1802 for ( int i = 0; i < insertResultList.size(); ++i )
1803 {
1804 QDomNodeList featureIdList = insertResultList.at( i ).toElement().elementsByTagNameNS( QgsWFSConstants::OGC_NAMESPACE, QStringLiteral( "FeatureId" ) );
1805 for ( int j = 0; j < featureIdList.size(); ++j )
1806 {
1807 QString fidString = featureIdList.at( j ).toElement().attribute( QStringLiteral( "fid" ) );
1808 if ( !fidString.isEmpty() )
1809 {
1810 ids << fidString;
1811 }
1812 }
1813 }
1814 return ids;
1815 }
1816
getCapabilities()1817 bool QgsWFSProvider::getCapabilities()
1818 {
1819 mCapabilities = QgsVectorDataProvider::SelectAtId;
1820
1821 if ( mShared->mCaps.version.isEmpty() )
1822 {
1823 QgsWfsCapabilities getCapabilities( mShared->mURI.uri() );
1824 const bool synchronous = true;
1825 const bool forceRefresh = false;
1826 if ( !getCapabilities.requestCapabilities( synchronous, forceRefresh ) )
1827 {
1828 QgsMessageLog::logMessage( tr( "GetCapabilities failed for url %1: %2" ).
1829 arg( dataSourceUri(), getCapabilities.errorMessage() ), tr( "WFS" ) );
1830 return false;
1831 }
1832
1833 const QgsWfsCapabilities::Capabilities caps = getCapabilities.capabilities();
1834 mShared->mCaps = caps;
1835 }
1836 mShared->mURI.setGetEndpoints( mShared->mCaps.operationGetEndpoints );
1837 mShared->mURI.setPostEndpoints( mShared->mCaps.operationPostEndpoints );
1838
1839 mShared->mWFSVersion = mShared->mCaps.version;
1840 if ( mShared->mURI.maxNumFeatures() > 0 && mShared->mCaps.maxFeatures > 0 && !( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() ) )
1841 {
1842 mShared->mMaxFeatures = std::min( mShared->mURI.maxNumFeatures(), mShared->mCaps.maxFeatures );
1843 }
1844 else if ( mShared->mURI.maxNumFeatures() > 0 )
1845 {
1846 mShared->mMaxFeatures = mShared->mURI.maxNumFeatures();
1847 }
1848 else if ( mShared->mCaps.maxFeatures > 0 && !( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() ) )
1849 {
1850 mShared->mMaxFeatures = mShared->mCaps.maxFeatures;
1851 }
1852 else
1853 {
1854 mShared->mMaxFeatures = 0;
1855 }
1856
1857 if ( mShared->mCaps.supportsPaging && mShared->mURI.pagingEnabled() )
1858 {
1859 if ( mShared->mURI.pageSize() > 0 )
1860 {
1861 if ( mShared->mCaps.maxFeatures > 0 )
1862 {
1863 mShared->mPageSize = std::min( mShared->mURI.pageSize(), mShared->mCaps.maxFeatures );
1864 }
1865 else
1866 {
1867 mShared->mPageSize = mShared->mURI.pageSize();
1868 }
1869 }
1870 else if ( mShared->mCaps.maxFeatures > 0 )
1871 {
1872 mShared->mPageSize = mShared->mCaps.maxFeatures;
1873 }
1874 else
1875 {
1876 QgsSettings settings;
1877 mShared->mPageSize = settings.value( QStringLiteral( "wfs/max_feature_count_if_not_provided" ), "1000" ).toInt();
1878 QgsDebugMsg( QStringLiteral( "Server declares paging but does not advertise max feature count and user did not specify it. Using %1" ).arg( mShared->mPageSize ) );
1879 }
1880 }
1881 else
1882 {
1883 mShared->mPageSize = 0;
1884 }
1885
1886 //find the <FeatureType> for this layer
1887 QString thisLayerName = mShared->mURI.typeName();
1888 bool foundLayer = false;
1889 for ( int i = 0; i < mShared->mCaps.featureTypes.size(); i++ )
1890 {
1891 if ( thisLayerName == mShared->mCaps.featureTypes[i].name )
1892 {
1893 const QgsRectangle &r = mShared->mCaps.featureTypes[i].bbox;
1894 if ( mShared->mSourceCrs.authid().isEmpty() && mShared->mCaps.featureTypes[i].crslist.size() != 0 )
1895 {
1896 mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mShared->mCaps.featureTypes[i].crslist[0] );
1897 }
1898 if ( !r.isNull() )
1899 {
1900 if ( mShared->mCaps.featureTypes[i].bboxSRSIsWGS84 )
1901 {
1902 QgsCoordinateReferenceSystem src = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "CRS:84" ) );
1903 QgsCoordinateTransform ct( src, mShared->mSourceCrs, transformContext() );
1904 QgsDebugMsgLevel( "latlon ext:" + r.toString(), 4 );
1905 QgsDebugMsgLevel( "src:" + src.authid(), 4 );
1906 QgsDebugMsgLevel( "dst:" + mShared->mSourceCrs.authid(), 4 );
1907
1908 mShared->mCapabilityExtent = ct.transformBoundingBox( r, QgsCoordinateTransform::ForwardTransform );
1909 }
1910 else
1911 {
1912 mShared->mCapabilityExtent = r;
1913 }
1914
1915 QgsDebugMsgLevel( "layer ext:" + mShared->mCapabilityExtent.toString(), 4 );
1916 }
1917 if ( mShared->mCaps.featureTypes[i].insertCap )
1918 {
1919 mCapabilities |= QgsVectorDataProvider::AddFeatures;
1920 }
1921 if ( mShared->mCaps.featureTypes[i].updateCap )
1922 {
1923 mCapabilities |= QgsVectorDataProvider::ChangeAttributeValues;
1924 mCapabilities |= QgsVectorDataProvider::ChangeGeometries;
1925 }
1926 if ( mShared->mCaps.featureTypes[i].deleteCap )
1927 {
1928 mCapabilities |= QgsVectorDataProvider::DeleteFeatures;
1929 }
1930
1931 foundLayer = true;
1932 }
1933 }
1934
1935 if ( !foundLayer )
1936 {
1937 QgsMessageLog::logMessage( tr( "Could not find typename %1 in capabilities for url %2" ).
1938 arg( thisLayerName, dataSourceUri() ), tr( "WFS" ) );
1939 }
1940
1941 return foundLayer;
1942 }
1943
geomTypeFromPropertyType(const QString & attName,const QString & propType)1944 QgsWkbTypes::Type QgsWFSProvider::geomTypeFromPropertyType( const QString &attName, const QString &propType )
1945 {
1946 Q_UNUSED( attName )
1947
1948 QgsDebugMsgLevel( QStringLiteral( "DescribeFeatureType geometry attribute \"%1\" type is \"%2\"" )
1949 .arg( attName, propType ), 4 );
1950 if ( propType == QLatin1String( "Point" ) )
1951 return QgsWkbTypes::Point;
1952 if ( propType == QLatin1String( "LineString" ) || propType == QLatin1String( "Curve" ) )
1953 return QgsWkbTypes::LineString;
1954 if ( propType == QLatin1String( "Polygon" ) || propType == QLatin1String( "Surface" ) )
1955 return QgsWkbTypes::Polygon;
1956 if ( propType == QLatin1String( "MultiPoint" ) )
1957 return QgsWkbTypes::MultiPoint;
1958 if ( propType == QLatin1String( "MultiLineString" ) || propType == QLatin1String( "MultiCurve" ) )
1959 return QgsWkbTypes::MultiLineString;
1960 if ( propType == QLatin1String( "MultiPolygon" ) || propType == QLatin1String( "MultiSurface" ) )
1961 return QgsWkbTypes::MultiPolygon;
1962 return QgsWkbTypes::Unknown;
1963 }
1964
handleException(const QDomDocument & serverResponse)1965 void QgsWFSProvider::handleException( const QDomDocument &serverResponse )
1966 {
1967 QgsDebugMsgLevel( QStringLiteral( "server response: %1" ).arg( serverResponse.toString() ), 4 );
1968
1969 QDomElement exceptionElem = serverResponse.documentElement();
1970 if ( exceptionElem.isNull() )
1971 {
1972 pushError( tr( "Empty response" ) );
1973 return;
1974 }
1975
1976 if ( exceptionElem.tagName() == QLatin1String( "ServiceExceptionReport" ) )
1977 {
1978 pushError( tr( "WFS service exception: %1" ).arg( exceptionElem.firstChildElement( QStringLiteral( "ServiceException" ) ).text() ) );
1979 return;
1980 }
1981
1982 if ( exceptionElem.tagName() == QLatin1String( "WFS_TransactionResponse" ) )
1983 {
1984 pushError( tr( "Unsuccessful service response: %1" ).arg( exceptionElem.firstChildElement( QStringLiteral( "TransactionResult" ) ).firstChildElement( QStringLiteral( "Message" ) ).text() ) );
1985 return;
1986 }
1987
1988 // WFS 1.1.0
1989 if ( exceptionElem.tagName() == QLatin1String( "TransactionResponse" ) )
1990 {
1991 pushError( tr( "Unsuccessful service response: no features were added, deleted or changed." ) );
1992 return;
1993 }
1994
1995
1996 if ( exceptionElem.tagName() == QLatin1String( "ExceptionReport" ) )
1997 {
1998 QDomElement exception = exceptionElem.firstChildElement( QStringLiteral( "Exception" ) );
1999 // The XML schema at http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd uses
2000 // the "exceptionCode" attribute, but http://docs.opengeospatial.org/is/04-094r1/04-094r1.html#36
2001 // mentions "code". Accept both...
2002 pushError( tr( "WFS exception report (code=%1 text=%2)" )
2003 .arg( exception.attribute( QStringLiteral( "exceptionCode" ),
2004 exception.attribute( QStringLiteral( "code" ), tr( "missing" ) ) ),
2005 exception.firstChildElement( QStringLiteral( "ExceptionText" ) ).text() )
2006 );
2007 return;
2008 }
2009
2010 pushError( tr( "Unhandled response: %1" ).arg( exceptionElem.tagName() ) );
2011 }
2012
createProvider(const QString & uri,const QgsDataProvider::ProviderOptions & options,QgsDataProvider::ReadFlags flags)2013 QgsWFSProvider *QgsWfsProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags )
2014 {
2015 Q_UNUSED( flags );
2016 return new QgsWFSProvider( uri, options );
2017 }
2018
dataItemProviders() const2019 QList<QgsDataItemProvider *> QgsWfsProviderMetadata::dataItemProviders() const
2020 {
2021 QList<QgsDataItemProvider *> providers;
2022 providers << new QgsWfsDataItemProvider;
2023 return providers;
2024 }
2025
2026
QgsWfsProviderMetadata()2027 QgsWfsProviderMetadata::QgsWfsProviderMetadata():
2028 QgsProviderMetadata( QgsWFSProvider::WFS_PROVIDER_KEY, QgsWFSProvider::WFS_PROVIDER_DESCRIPTION ) {}
2029
multipleProviderMetadataFactory()2030 QGISEXTERN void *multipleProviderMetadataFactory()
2031 {
2032 return new std::vector<QgsProviderMetadata *> { new QgsWfsProviderMetadata(), new QgsOapifProviderMetadata() };
2033 }
2034