1 /*************************************************************************** 2 qgswfsgetfeature.cpp 3 ------------------------- 4 begin : December 20 , 2016 5 copyright : (C) 2007 by Marco Hugentobler (original code) 6 (C) 2012 by René-Luc D'Hont (original code) 7 (C) 2014 by Alessandro Pasotti (original code) 8 (C) 2017 by David Marteau 9 email : marco dot hugentobler at karto dot baug dot ethz dot ch 10 a dot pasotti at itopen dot it 11 david dot marteau at 3liz dot com 12 ***************************************************************************/ 13 14 /*************************************************************************** 15 * * 16 * This program is free software; you can redistribute it and/or modify * 17 * it under the terms of the GNU General Public License as published by * 18 * the Free Software Foundation; either version 2 of the License, or * 19 * (at your option) any later version. * 20 * * 21 ***************************************************************************/ 22 #include "qgswfsutils.h" 23 #include "qgsserverprojectutils.h" 24 #include "qgsserverfeatureid.h" 25 #include "qgsfields.h" 26 #include "qgsdatetimefieldformatter.h" 27 #include "qgsexpression.h" 28 #include "qgsgeometry.h" 29 #include "qgsmaplayer.h" 30 #include "qgsfeatureiterator.h" 31 #include "qgscoordinatereferencesystem.h" 32 #include "qgsvectorlayer.h" 33 #include "qgsfilterrestorer.h" 34 #include "qgsproject.h" 35 #include "qgsogcutils.h" 36 #include "qgsjsonutils.h" 37 #include "qgsexpressioncontextutils.h" 38 #include "qgswkbtypes.h" 39 40 #include "qgswfsgetfeature.h" 41 42 namespace QgsWfs 43 { 44 45 namespace 46 { 47 struct createFeatureParams 48 { 49 int precision; 50 51 const QgsCoordinateReferenceSystem &crs; 52 53 const QgsAttributeList &attributeIndexes; 54 55 const QString &typeName; 56 57 bool withGeom; 58 59 const QString &geometryName; 60 61 const QgsCoordinateReferenceSystem &outputCrs; 62 63 bool forceGeomToMulti; 64 65 const QString &srsName; 66 67 bool hasAxisInverted; 68 }; 69 70 QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams ¶ms, const QgsAttributeList &pkAttributes ); 71 72 QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup ); 73 74 QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ); 75 76 QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ); 77 78 void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, 79 QgsWfsParameters::Format format, int numberOfFeatures, const QStringList &typeNames, const QgsServerSettings *serverSettings ); 80 81 void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, 82 QgsWfsParameters::Format format, int prec, QgsCoordinateReferenceSystem &crs, 83 QgsRectangle *rect, const QStringList &typeNames, const QgsServerSettings *settings ); 84 85 void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx, 86 const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes = QgsAttributeList() ); 87 88 void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format ); 89 90 QgsServerRequest::Parameters mRequestParameters; 91 QgsWfsParameters mWfsParameters; 92 /* GeoJSON Exporter */ 93 QgsJsonExporter mJsonExporter; 94 } 95 writeGetFeature(QgsServerInterface * serverIface,const QgsProject * project,const QString & version,const QgsServerRequest & request,QgsServerResponse & response)96 void writeGetFeature( QgsServerInterface *serverIface, const QgsProject *project, 97 const QString &version, const QgsServerRequest &request, 98 QgsServerResponse &response ) 99 { 100 Q_UNUSED( version ) 101 102 mRequestParameters = request.parameters(); 103 mWfsParameters = QgsWfsParameters( QUrlQuery( request.url() ) ); 104 mWfsParameters.dump(); 105 getFeatureRequest aRequest; 106 107 QDomDocument doc; 108 QString errorMsg; 109 110 if ( doc.setContent( request.data(), true, &errorMsg ) ) 111 { 112 QDomElement docElem = doc.documentElement(); 113 aRequest = parseGetFeatureRequestBody( docElem, project ); 114 } 115 else 116 { 117 aRequest = parseGetFeatureParameters( project ); 118 } 119 120 // store typeName 121 QStringList typeNameList; 122 123 // Request metadata 124 bool onlyOneLayer = ( aRequest.queries.size() == 1 ); 125 QgsRectangle requestRect; 126 QgsCoordinateReferenceSystem requestCrs; 127 int requestPrecision = 6; 128 if ( !onlyOneLayer ) 129 requestCrs = QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ); 130 131 QList<getFeatureQuery>::iterator qIt = aRequest.queries.begin(); 132 for ( ; qIt != aRequest.queries.end(); ++qIt ) 133 { 134 typeNameList << ( *qIt ).typeName; 135 } 136 137 // get layers and 138 // update the request metadata 139 QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project ); 140 QMap<QString, QgsMapLayer *> mapLayerMap; 141 for ( int i = 0; i < wfsLayerIds.size(); ++i ) 142 { 143 QgsMapLayer *layer = project->mapLayer( wfsLayerIds.at( i ) ); 144 if ( !layer ) 145 { 146 continue; 147 } 148 if ( layer->type() != QgsMapLayerType::VectorLayer ) 149 { 150 continue; 151 } 152 153 QString name = layerTypeName( layer ); 154 155 if ( typeNameList.contains( name ) ) 156 { 157 // store layers 158 mapLayerMap[name] = layer; 159 // update request metadata 160 if ( onlyOneLayer ) 161 { 162 requestRect = layer->extent(); 163 requestCrs = layer->crs(); 164 } 165 else 166 { 167 QgsCoordinateTransform transform( layer->crs(), requestCrs, project ); 168 try 169 { 170 if ( requestRect.isEmpty() ) 171 { 172 requestRect = transform.transform( layer->extent() ); 173 } 174 else 175 { 176 requestRect.combineExtentWith( transform.transform( layer->extent() ) ); 177 } 178 } 179 catch ( QgsException &cse ) 180 { 181 Q_UNUSED( cse ) 182 requestRect = QgsRectangle( -180.0, -90.0, 180.0, 90.0 ); 183 } 184 } 185 } 186 } 187 188 #ifdef HAVE_SERVER_PYTHON_PLUGINS 189 QgsAccessControl *accessControl = serverIface->accessControls(); 190 //scoped pointer to restore all original layer filters (subsetStrings) when pointer goes out of scope 191 //there's LOTS of potential exit paths here, so we avoid having to restore the filters manually 192 std::unique_ptr< QgsOWSServerFilterRestorer > filterRestorer( new QgsOWSServerFilterRestorer() ); 193 #else 194 ( void )serverIface; 195 #endif 196 197 // features counters 198 long sentFeatures = 0; 199 long iteratedFeatures = 0; 200 // sent features 201 QgsFeature feature; 202 qIt = aRequest.queries.begin(); 203 for ( ; qIt != aRequest.queries.end(); ++qIt ) 204 { 205 getFeatureQuery &query = *qIt; 206 QString typeName = query.typeName; 207 208 if ( !mapLayerMap.contains( typeName ) ) 209 { 210 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' unknown" ).arg( typeName ) ); 211 } 212 213 QgsMapLayer *layer = mapLayerMap[typeName]; 214 #ifdef HAVE_SERVER_PYTHON_PLUGINS 215 if ( accessControl && !accessControl->layerReadPermission( layer ) ) 216 { 217 throw QgsSecurityAccessException( QStringLiteral( "Feature access permission denied" ) ); 218 } 219 #endif 220 QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer ); 221 if ( !vlayer ) 222 { 223 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer error" ).arg( typeName ) ); 224 } 225 226 //test provider 227 QgsVectorDataProvider *provider = vlayer->dataProvider(); 228 if ( !provider ) 229 { 230 throw QgsRequestNotWellFormedException( QStringLiteral( "TypeName '%1' layer's provider error" ).arg( typeName ) ); 231 } 232 #ifdef HAVE_SERVER_PYTHON_PLUGINS 233 if ( accessControl ) 234 { 235 QgsOWSServerFilterRestorer::applyAccessControlLayerFilters( accessControl, vlayer, filterRestorer->originalFilters() ); 236 } 237 #endif 238 //is there alias info for this vector layer? 239 QMap< int, QString > layerAliasInfo; 240 QgsStringMap aliasMap = vlayer->attributeAliases(); 241 QgsStringMap::const_iterator aliasIt = aliasMap.constBegin(); 242 for ( ; aliasIt != aliasMap.constEnd(); ++aliasIt ) 243 { 244 int attrIndex = vlayer->fields().lookupField( aliasIt.key() ); 245 if ( attrIndex != -1 ) 246 { 247 layerAliasInfo.insert( attrIndex, aliasIt.value() ); 248 } 249 } 250 251 // get propertyList from query 252 const QStringList propertyList = query.propertyList; 253 254 //Using pending attributes and pending fields 255 QgsAttributeList attrIndexes = vlayer->attributeList(); 256 const QgsFields fields = vlayer->fields(); 257 bool withGeom = true; 258 if ( !propertyList.isEmpty() && propertyList.first() != QLatin1String( "*" ) ) 259 { 260 withGeom = false; 261 QStringList::const_iterator plstIt; 262 QList<int> idxList; 263 // build corresponding propertyname 264 QList<QString> propertynames; 265 QList<QString> fieldnames; 266 for ( const QgsField &field : fields ) 267 { 268 fieldnames.append( field.name() ); 269 propertynames.append( field.name().replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 270 } 271 QString fieldName; 272 for ( plstIt = propertyList.constBegin(); plstIt != propertyList.constEnd(); ++plstIt ) 273 { 274 fieldName = *plstIt; 275 int fieldNameIdx = propertynames.indexOf( fieldName ); 276 if ( fieldNameIdx == -1 ) 277 { 278 fieldNameIdx = fieldnames.indexOf( fieldName ); 279 } 280 if ( fieldNameIdx > -1 ) 281 { 282 idxList.append( fieldNameIdx ); 283 } 284 else if ( fieldName == QLatin1String( "geometry" ) ) 285 { 286 withGeom = true; 287 } 288 } 289 if ( !idxList.isEmpty() ) 290 { 291 attrIndexes = idxList; 292 } 293 } 294 295 //excluded attributes for this layer 296 if ( !attrIndexes.isEmpty() ) 297 { 298 for ( const QgsField &field : fields ) 299 { 300 if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::HideFromWfs ) ) 301 { 302 int fieldNameIdx = fields.indexOf( field.name() ); 303 if ( fieldNameIdx > -1 && attrIndexes.contains( fieldNameIdx ) ) 304 { 305 attrIndexes.removeOne( fieldNameIdx ); 306 } 307 } 308 } 309 } 310 311 // update request 312 QgsFeatureRequest featureRequest = query.featureRequest; 313 314 // expression context 315 QgsExpressionContext expressionContext; 316 expressionContext << QgsExpressionContextUtils::globalScope() 317 << QgsExpressionContextUtils::projectScope( project ) 318 << QgsExpressionContextUtils::layerScope( vlayer ); 319 featureRequest.setExpressionContext( expressionContext ); 320 321 if ( !query.serverFids.isEmpty() ) 322 { 323 QgsServerFeatureId::updateFeatureRequestFromServerFids( featureRequest, query.serverFids, provider ); 324 } 325 326 // geometry flags 327 if ( vlayer->wkbType() == QgsWkbTypes::NoGeometry ) 328 featureRequest.setFlags( featureRequest.flags() | QgsFeatureRequest::NoGeometry ); 329 else 330 featureRequest.setFlags( featureRequest.flags() | ( withGeom ? QgsFeatureRequest::NoFlags : QgsFeatureRequest::NoGeometry ) ); 331 332 // subset of attributes 333 featureRequest.setSubsetOfAttributes( attrIndexes ); 334 #ifdef HAVE_SERVER_PYTHON_PLUGINS 335 if ( accessControl ) 336 { 337 accessControl->filterFeatures( vlayer, featureRequest ); 338 339 QStringList attributes = QStringList(); 340 for ( int idx : std::as_const( attrIndexes ) ) 341 { 342 attributes.append( vlayer->fields().field( idx ).name() ); 343 } 344 featureRequest.setSubsetOfAttributes( 345 accessControl->layerAttributes( vlayer, attributes ), 346 vlayer->fields() ); 347 attrIndexes = featureRequest.subsetOfAttributes(); 348 } 349 #endif 350 351 // Force pkAttributes in subset of attributes for primary fid building 352 const QgsAttributeList pkAttributes = provider->pkAttributeIndexes(); 353 if ( !pkAttributes.isEmpty() ) 354 { 355 QgsAttributeList subsetOfAttrs = featureRequest.subsetOfAttributes(); 356 for ( int idx : pkAttributes ) 357 { 358 if ( !subsetOfAttrs.contains( idx ) ) 359 { 360 subsetOfAttrs.prepend( idx ); 361 } 362 } 363 if ( subsetOfAttrs.size() != featureRequest.subsetOfAttributes().size() ) 364 { 365 featureRequest.setSubsetOfAttributes( subsetOfAttrs ); 366 } 367 } 368 369 if ( onlyOneLayer ) 370 { 371 requestPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() ); 372 } 373 374 if ( aRequest.maxFeatures > 0 ) 375 { 376 featureRequest.setLimit( aRequest.maxFeatures + aRequest.startIndex - sentFeatures ); 377 } 378 // specific layer precision 379 int layerPrecision = QgsServerProjectUtils::wfsLayerPrecision( *project, vlayer->id() ); 380 // specific layer crs 381 QgsCoordinateReferenceSystem layerCrs = vlayer->crs(); 382 383 // Geometry name 384 QString geometryName = aRequest.geometryName; 385 if ( !withGeom ) 386 { 387 geometryName = QLatin1String( "NONE" ); 388 } 389 // outputCrs 390 QgsCoordinateReferenceSystem outputCrs = vlayer->crs(); 391 if ( !query.srsName.isEmpty() ) 392 { 393 outputCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( query.srsName ); 394 } 395 396 bool forceGeomToMulti = QgsWkbTypes::isMultiType( vlayer->wkbType() ); 397 398 if ( !featureRequest.filterRect().isEmpty() ) 399 { 400 QgsCoordinateTransform transform( outputCrs, vlayer->crs(), project ); 401 try 402 { 403 featureRequest.setFilterRect( transform.transform( featureRequest.filterRect() ) ); 404 } 405 catch ( QgsException &cse ) 406 { 407 Q_UNUSED( cse ) 408 } 409 if ( onlyOneLayer ) 410 { 411 requestRect = featureRequest.filterRect(); 412 } 413 } 414 415 // Iterate through features 416 QgsFeatureIterator fit = vlayer->getFeatures( featureRequest ); 417 418 if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS ) 419 { 420 while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) ) 421 { 422 if ( iteratedFeatures >= aRequest.startIndex ) 423 { 424 ++sentFeatures; 425 } 426 ++iteratedFeatures; 427 } 428 } 429 else 430 { 431 432 // For WFS 1.1 we honor requested CRS and axis order 433 const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; 434 const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && 435 outputCrs.hasAxisInverted() && 436 ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; 437 438 const createFeatureParams cfp = { layerPrecision, 439 layerCrs, 440 attrIndexes, 441 typeName, 442 withGeom, 443 geometryName, 444 outputCrs, 445 forceGeomToMulti, 446 srsName, 447 invertAxis 448 }; 449 while ( fit.nextFeature( feature ) && ( aRequest.maxFeatures == -1 || sentFeatures < aRequest.maxFeatures ) ) 450 { 451 if ( iteratedFeatures == aRequest.startIndex ) 452 startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList, serverIface->serverSettings() ); 453 454 if ( iteratedFeatures >= aRequest.startIndex ) 455 { 456 setGetFeature( response, aRequest.outputFormat, feature, sentFeatures, cfp, project, provider->pkAttributeIndexes() ); 457 ++sentFeatures; 458 } 459 ++iteratedFeatures; 460 } 461 } 462 } 463 464 #ifdef HAVE_SERVER_PYTHON_PLUGINS 465 //force restoration of original layer filters 466 filterRestorer.reset(); 467 #endif 468 469 if ( mWfsParameters.resultType() == QgsWfsParameters::ResultType::HITS ) 470 { 471 hitGetFeature( request, response, project, aRequest.outputFormat, sentFeatures, typeNameList, serverIface->serverSettings() ); 472 } 473 else 474 { 475 // End of GetFeature 476 if ( iteratedFeatures <= aRequest.startIndex ) 477 startGetFeature( request, response, project, aRequest.outputFormat, requestPrecision, requestCrs, &requestRect, typeNameList, serverIface->serverSettings() ); 478 endGetFeature( response, aRequest.outputFormat ); 479 } 480 481 } 482 parseGetFeatureParameters(const QgsProject * project)483 getFeatureRequest parseGetFeatureParameters( const QgsProject *project ) 484 { 485 getFeatureRequest request; 486 request.maxFeatures = mWfsParameters.maxFeaturesAsInt(); 487 request.startIndex = mWfsParameters.startIndexAsInt(); 488 request.outputFormat = mWfsParameters.outputFormat(); 489 490 // Verifying parameters mutually exclusive 491 QStringList fidList = mWfsParameters.featureIds(); 492 bool paramContainsFeatureIds = !fidList.isEmpty(); 493 QStringList filterList = mWfsParameters.filters(); 494 bool paramContainsFilters = !filterList.isEmpty(); 495 QString bbox = mWfsParameters.bbox(); 496 bool paramContainsBbox = !bbox.isEmpty(); 497 if ( ( paramContainsFeatureIds 498 && ( paramContainsFilters || paramContainsBbox ) ) 499 || ( paramContainsFilters 500 && ( paramContainsFeatureIds || paramContainsBbox ) ) 501 || ( paramContainsBbox 502 && ( paramContainsFeatureIds || paramContainsFilters ) ) 503 ) 504 { 505 throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID FILTER and BBOX parameters are mutually exclusive" ) ); 506 } 507 508 // Get and split PROPERTYNAME parameter 509 QStringList propertyNameList = mWfsParameters.propertyNames(); 510 511 // Manage extra parameter GeometryName 512 request.geometryName = mWfsParameters.geometryNameAsString().toUpper(); 513 514 QStringList typeNameList; 515 // parse FEATUREID 516 if ( paramContainsFeatureIds ) 517 { 518 // Verifying the 1:1 mapping between FEATUREID and PROPERTYNAME 519 if ( !propertyNameList.isEmpty() && propertyNameList.size() != fidList.size() ) 520 { 521 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a FEATUREID and the PROPERTYNAME list" ) ); 522 } 523 if ( propertyNameList.isEmpty() ) 524 { 525 for ( int i = 0; i < fidList.size(); ++i ) 526 { 527 propertyNameList << QStringLiteral( "*" ); 528 } 529 } 530 531 QMap<QString, QStringList> fidsMap; 532 533 QStringList::const_iterator fidIt = fidList.constBegin(); 534 QStringList::const_iterator propertyNameIt = propertyNameList.constBegin(); 535 for ( ; fidIt != fidList.constEnd(); ++fidIt ) 536 { 537 // Get FeatureID 538 QString fid = *fidIt; 539 fid = fid.trimmed(); 540 // Get PropertyName for this FeatureID 541 QString propertyName; 542 if ( propertyNameIt != propertyNameList.constEnd() ) 543 { 544 propertyName = *propertyNameIt; 545 } 546 // testing typename in the WFS featureID 547 if ( !fid.contains( '.' ) ) 548 { 549 throw QgsRequestNotWellFormedException( QStringLiteral( "FEATUREID has to have TYPENAME in the values" ) ); 550 } 551 552 QString typeName = fid.section( '.', 0, 0 ); 553 fid = fid.section( '.', 1, 1 ); 554 if ( !typeNameList.contains( typeName ) ) 555 { 556 typeNameList << typeName; 557 } 558 559 // each Feature requested by FEATUREID can have each own property list 560 QString key = QStringLiteral( "%1(%2)" ).arg( typeName, propertyName ); 561 QStringList fids; 562 if ( fidsMap.contains( key ) ) 563 { 564 fids = fidsMap.value( key ); 565 } 566 fids.append( fid ); 567 fidsMap.insert( key, fids ); 568 569 if ( propertyNameIt != propertyNameList.constEnd() ) 570 { 571 ++propertyNameIt; 572 } 573 } 574 575 QMap<QString, QStringList>::const_iterator fidsMapIt = fidsMap.constBegin(); 576 while ( fidsMapIt != fidsMap.constEnd() ) 577 { 578 QString key = fidsMapIt.key(); 579 580 //Extract TypeName and PropertyName from key 581 QRegExp rx( "([^()]+)\\(([^()]+)\\)" ); 582 if ( rx.indexIn( key, 0 ) == -1 ) 583 { 584 throw QgsRequestNotWellFormedException( QStringLiteral( "Error getting properties for FEATUREID" ) ); 585 } 586 QString typeName = rx.cap( 1 ); 587 QString propertyName = rx.cap( 2 ); 588 589 getFeatureQuery query; 590 query.typeName = typeName; 591 query.srsName = mWfsParameters.srsName(); 592 593 // Parse PropertyName 594 if ( propertyName != QLatin1String( "*" ) ) 595 { 596 QStringList propertyList; 597 598 const QStringList attrList = propertyName.split( ',' ); 599 QStringList::const_iterator alstIt; 600 for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt ) 601 { 602 QString fieldName = *alstIt; 603 fieldName = fieldName.trimmed(); 604 if ( fieldName.contains( ':' ) ) 605 { 606 fieldName = fieldName.section( ':', 1, 1 ); 607 } 608 if ( fieldName.contains( '/' ) ) 609 { 610 if ( fieldName.section( '/', 0, 0 ) != typeName ) 611 { 612 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 613 } 614 fieldName = fieldName.section( '/', 1, 1 ); 615 } 616 propertyList.append( fieldName ); 617 } 618 query.propertyList = propertyList; 619 } 620 621 query.serverFids = fidsMapIt.value(); 622 QgsFeatureRequest featureRequest; 623 624 query.featureRequest = featureRequest; 625 request.queries.append( query ); 626 ++fidsMapIt; 627 } 628 return request; 629 } 630 631 if ( !mRequestParameters.contains( QStringLiteral( "TYPENAME" ) ) ) 632 { 633 throw QgsRequestNotWellFormedException( QStringLiteral( "TYPENAME is mandatory except if FEATUREID is used" ) ); 634 } 635 636 typeNameList = mWfsParameters.typeNames(); 637 // Verifying the 1:1 mapping between TYPENAME and PROPERTYNAME 638 if ( !propertyNameList.isEmpty() && typeNameList.size() != propertyNameList.size() ) 639 { 640 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the PROPERTYNAME list" ) ); 641 } 642 if ( propertyNameList.isEmpty() ) 643 { 644 for ( int i = 0; i < typeNameList.size(); ++i ) 645 { 646 propertyNameList << QStringLiteral( "*" ); 647 } 648 } 649 650 // Create queries based on TypeName and propertyName 651 QStringList::const_iterator typeNameIt = typeNameList.constBegin(); 652 QStringList::const_iterator propertyNameIt = propertyNameList.constBegin(); 653 for ( ; typeNameIt != typeNameList.constEnd(); ++typeNameIt ) 654 { 655 QString typeName = *typeNameIt; 656 typeName = typeName.trimmed(); 657 // Get PropertyName for this typeName 658 QString propertyName; 659 if ( propertyNameIt != propertyNameList.constEnd() ) 660 { 661 propertyName = *propertyNameIt; 662 } 663 664 getFeatureQuery query; 665 query.typeName = typeName; 666 query.srsName = mWfsParameters.srsName(); 667 668 // Parse PropertyName 669 if ( propertyName != QLatin1String( "*" ) ) 670 { 671 QStringList propertyList; 672 673 const QStringList attrList = propertyName.split( ',' ); 674 QStringList::const_iterator alstIt; 675 for ( alstIt = attrList.constBegin(); alstIt != attrList.constEnd(); ++alstIt ) 676 { 677 QString fieldName = *alstIt; 678 fieldName = fieldName.trimmed(); 679 if ( fieldName.contains( ':' ) ) 680 { 681 fieldName = fieldName.section( ':', 1, 1 ); 682 } 683 if ( fieldName.contains( '/' ) ) 684 { 685 if ( fieldName.section( '/', 0, 0 ) != typeName ) 686 { 687 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 688 } 689 fieldName = fieldName.section( '/', 1, 1 ); 690 } 691 propertyList.append( fieldName ); 692 } 693 query.propertyList = propertyList; 694 } 695 696 request.queries.append( query ); 697 698 if ( propertyNameIt != propertyNameList.constEnd() ) 699 { 700 ++propertyNameIt; 701 } 702 } 703 704 // Manage extra parameter exp_filter 705 QStringList expFilterList = mWfsParameters.expFilters(); 706 if ( !expFilterList.isEmpty() ) 707 { 708 // Verifying the 1:1 mapping between TYPENAME and EXP_FILTER but without exception 709 if ( request.queries.size() == expFilterList.size() ) 710 { 711 // set feature request filter expression based on filter element 712 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 713 QStringList::const_iterator expFilterIt = expFilterList.constBegin(); 714 for ( ; qIt != request.queries.end(); ++qIt ) 715 { 716 getFeatureQuery &query = *qIt; 717 // Get Filter for this typeName 718 QString expFilter; 719 if ( expFilterIt != expFilterList.constEnd() ) 720 { 721 expFilter = *expFilterIt; 722 } 723 std::shared_ptr<QgsExpression> filter( new QgsExpression( expFilter ) ); 724 if ( filter ) 725 { 726 if ( filter->hasParserError() ) 727 { 728 throw QgsRequestNotWellFormedException( QStringLiteral( "The EXP_FILTER expression has errors: %1" ).arg( filter->parserErrorString() ) ); 729 } 730 if ( filter->needsGeometry() ) 731 { 732 query.featureRequest.setFlags( QgsFeatureRequest::NoFlags ); 733 } 734 query.featureRequest.setFilterExpression( filter->expression() ); 735 } 736 } 737 } 738 else 739 { 740 QgsMessageLog::logMessage( "There has to be a 1:1 mapping between each element in a TYPENAME and the EXP_FILTER list" ); 741 } 742 } 743 744 if ( paramContainsBbox ) 745 { 746 747 // get bbox extent 748 QgsRectangle extent = mWfsParameters.bboxAsRectangle(); 749 750 QString extentSrsName { mWfsParameters.srsName() }; 751 752 // handle WFS 1.1.0 optional CRS 753 if ( mWfsParameters.bbox().split( ',' ).size() == 5 && ! mWfsParameters.srsName().isEmpty() ) 754 { 755 QString crs( mWfsParameters.bbox().split( ',' )[4] ); 756 if ( crs != mWfsParameters.srsName() ) 757 { 758 extentSrsName = crs; 759 QgsCoordinateReferenceSystem sourceCrs( crs ); 760 QgsCoordinateReferenceSystem destinationCrs( mWfsParameters.srsName() ); 761 if ( sourceCrs.isValid() && destinationCrs.isValid( ) ) 762 { 763 QgsGeometry extentGeom = QgsGeometry::fromRect( extent ); 764 QgsCoordinateTransform transform; 765 transform.setSourceCrs( sourceCrs ); 766 transform.setDestinationCrs( destinationCrs ); 767 try 768 { 769 if ( extentGeom.transform( transform ) == Qgis::GeometryOperationResult::Success ) 770 { 771 extent = QgsRectangle( extentGeom.boundingBox() ); 772 } 773 } 774 catch ( QgsException &cse ) 775 { 776 Q_UNUSED( cse ) 777 } 778 } 779 } 780 } 781 782 // Follow GeoServer conventions and handle axis order 783 // See: https://docs.geoserver.org/latest/en/user/services/wfs/axis_order.html#wfs-basics-axis 784 QgsCoordinateReferenceSystem extentCrs; 785 extentCrs.createFromUserInput( extentSrsName ); 786 if ( extentCrs.isValid() && extentCrs.hasAxisInverted() && ! extentSrsName.startsWith( QLatin1String( "EPSG:" ) ) ) 787 { 788 QgsGeometry geom { QgsGeometry::fromRect( extent ) }; 789 geom.get()->swapXy(); 790 extent = geom.boundingBox(); 791 } 792 793 // set feature request filter rectangle 794 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 795 for ( ; qIt != request.queries.end(); ++qIt ) 796 { 797 getFeatureQuery &query = *qIt; 798 query.featureRequest.setFilterRect( extent ).setFlags( query.featureRequest.flags() | QgsFeatureRequest::ExactIntersect ); 799 } 800 return request; 801 } 802 else if ( paramContainsFilters ) 803 { 804 // Verifying the 1:1 mapping between TYPENAME and FILTER 805 if ( request.queries.size() != filterList.size() ) 806 { 807 throw QgsRequestNotWellFormedException( QStringLiteral( "There has to be a 1:1 mapping between each element in a TYPENAME and the FILTER list" ) ); 808 } 809 810 // set feature request filter expression based on filter element 811 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 812 QStringList::const_iterator filterIt = filterList.constBegin(); 813 for ( ; qIt != request.queries.end(); ++qIt ) 814 { 815 getFeatureQuery &query = *qIt; 816 // Get Filter for this typeName 817 QDomDocument filter; 818 if ( filterIt != filterList.constEnd() ) 819 { 820 QString errorMsg; 821 if ( !filter.setContent( *filterIt, true, &errorMsg ) ) 822 { 823 throw QgsRequestNotWellFormedException( QStringLiteral( "error message: %1. The XML string was: %2" ).arg( errorMsg, *filterIt ) ); 824 } 825 } 826 827 QDomElement filterElem = filter.firstChildElement(); 828 QStringList serverFids; 829 query.featureRequest = parseFilterElement( query.typeName, filterElem, serverFids, project ); 830 query.serverFids = serverFids; 831 832 if ( filterIt != filterList.constEnd() ) 833 { 834 ++filterIt; 835 } 836 } 837 return request; 838 } 839 840 QStringList sortByList = mWfsParameters.sortBy(); 841 if ( !sortByList.isEmpty() && request.queries.size() == sortByList.size() ) 842 { 843 // add order by to feature request 844 QList<getFeatureQuery>::iterator qIt = request.queries.begin(); 845 QStringList::const_iterator sortByIt = sortByList.constBegin(); 846 for ( ; qIt != request.queries.end(); ++qIt ) 847 { 848 getFeatureQuery &query = *qIt; 849 // Get sortBy for this typeName 850 QString sortBy; 851 if ( sortByIt != sortByList.constEnd() ) 852 { 853 sortBy = *sortByIt; 854 } 855 for ( const QString &attribute : sortBy.split( ',' ) ) 856 { 857 if ( attribute.endsWith( QLatin1String( " D" ) ) || attribute.endsWith( QLatin1String( "+D" ) ) ) 858 { 859 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ), false ); 860 } 861 else if ( attribute.endsWith( QLatin1String( " DESC" ) ) || attribute.endsWith( QLatin1String( "+DESC" ) ) ) 862 { 863 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 5 ), false ); 864 } 865 else if ( attribute.endsWith( QLatin1String( " A" ) ) || attribute.endsWith( QLatin1String( "+A" ) ) ) 866 { 867 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 2 ) ); 868 } 869 else if ( attribute.endsWith( QLatin1String( " ASC" ) ) || attribute.endsWith( QLatin1String( "+ASC" ) ) ) 870 { 871 query.featureRequest.addOrderBy( attribute.left( attribute.size() - 4 ) ); 872 } 873 else 874 { 875 query.featureRequest.addOrderBy( attribute ); 876 } 877 } 878 } 879 } 880 881 return request; 882 } 883 parseGetFeatureRequestBody(QDomElement & docElem,const QgsProject * project)884 getFeatureRequest parseGetFeatureRequestBody( QDomElement &docElem, const QgsProject *project ) 885 { 886 getFeatureRequest request; 887 request.maxFeatures = mWfsParameters.maxFeaturesAsInt(); 888 request.startIndex = mWfsParameters.startIndexAsInt(); 889 request.outputFormat = mWfsParameters.outputFormat(); 890 891 QDomNodeList queryNodes = docElem.elementsByTagName( QStringLiteral( "Query" ) ); 892 QDomElement queryElem; 893 for ( int i = 0; i < queryNodes.size(); i++ ) 894 { 895 queryElem = queryNodes.at( i ).toElement(); 896 getFeatureQuery query = parseQueryElement( queryElem, project ); 897 request.queries.append( query ); 898 } 899 return request; 900 } 901 parseSortByElement(QDomElement & sortByElem,QgsFeatureRequest & featureRequest,const QString & typeName)902 void parseSortByElement( QDomElement &sortByElem, QgsFeatureRequest &featureRequest, const QString &typeName ) 903 { 904 QDomNodeList sortByNodes = sortByElem.childNodes(); 905 if ( sortByNodes.size() ) 906 { 907 for ( int i = 0; i < sortByNodes.size(); i++ ) 908 { 909 QDomElement sortPropElem = sortByNodes.at( i ).toElement(); 910 QDomNodeList sortPropChildNodes = sortPropElem.childNodes(); 911 if ( sortPropChildNodes.size() ) 912 { 913 QString fieldName; 914 bool ascending = true; 915 for ( int j = 0; j < sortPropChildNodes.size(); j++ ) 916 { 917 QDomElement sortPropChildElem = sortPropChildNodes.at( j ).toElement(); 918 if ( sortPropChildElem.tagName() == QLatin1String( "PropertyName" ) ) 919 { 920 fieldName = sortPropChildElem.text().trimmed(); 921 } 922 else if ( sortPropChildElem.tagName() == QLatin1String( "SortOrder" ) ) 923 { 924 QString sortOrder = sortPropChildElem.text().trimmed().toUpper(); 925 if ( sortOrder == QLatin1String( "DESC" ) || sortOrder == QLatin1String( "D" ) ) 926 ascending = false; 927 } 928 } 929 // clean fieldName 930 if ( fieldName.contains( ':' ) ) 931 { 932 fieldName = fieldName.section( ':', 1, 1 ); 933 } 934 if ( fieldName.contains( '/' ) ) 935 { 936 if ( fieldName.section( '/', 0, 0 ) != typeName ) 937 { 938 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 939 } 940 fieldName = fieldName.section( '/', 1, 1 ); 941 } 942 // addOrderBy 943 if ( !fieldName.isEmpty() ) 944 featureRequest.addOrderBy( fieldName, ascending ); 945 } 946 } 947 } 948 } 949 parseQueryElement(QDomElement & queryElem,const QgsProject * project)950 getFeatureQuery parseQueryElement( QDomElement &queryElem, const QgsProject *project ) 951 { 952 QString typeName = queryElem.attribute( QStringLiteral( "typeName" ), QString() ); 953 if ( typeName.contains( ':' ) ) 954 { 955 typeName = typeName.section( ':', 1, 1 ); 956 } 957 958 QgsFeatureRequest featureRequest; 959 QStringList serverFids; 960 QStringList propertyList; 961 QDomNodeList queryChildNodes = queryElem.childNodes(); 962 if ( queryChildNodes.size() ) 963 { 964 QDomElement sortByElem; 965 for ( int q = 0; q < queryChildNodes.size(); q++ ) 966 { 967 QDomElement queryChildElem = queryChildNodes.at( q ).toElement(); 968 if ( queryChildElem.tagName() == QLatin1String( "PropertyName" ) ) 969 { 970 QString fieldName = queryChildElem.text().trimmed(); 971 if ( fieldName.contains( ':' ) ) 972 { 973 fieldName = fieldName.section( ':', 1, 1 ); 974 } 975 if ( fieldName.contains( '/' ) ) 976 { 977 if ( fieldName.section( '/', 0, 0 ) != typeName ) 978 { 979 throw QgsRequestNotWellFormedException( QStringLiteral( "PropertyName text '%1' has to contain TypeName '%2'" ).arg( fieldName ).arg( typeName ) ); 980 } 981 fieldName = fieldName.section( '/', 1, 1 ); 982 } 983 propertyList.append( fieldName ); 984 } 985 else if ( queryChildElem.tagName() == QLatin1String( "Filter" ) ) 986 { 987 featureRequest = parseFilterElement( typeName, queryChildElem, serverFids, project ); 988 } 989 else if ( queryChildElem.tagName() == QLatin1String( "SortBy" ) ) 990 { 991 sortByElem = queryChildElem; 992 } 993 } 994 parseSortByElement( sortByElem, featureRequest, typeName ); 995 } 996 997 // srsName attribute 998 QString srsName = queryElem.attribute( QStringLiteral( "srsName" ), QString() ); 999 1000 getFeatureQuery query; 1001 query.typeName = typeName; 1002 query.srsName = srsName; 1003 query.featureRequest = featureRequest; 1004 query.serverFids = serverFids; 1005 query.propertyList = propertyList; 1006 return query; 1007 } 1008 1009 namespace 1010 { 1011 static QSet< QString > sParamFilter 1012 { 1013 QStringLiteral( "REQUEST" ), 1014 QStringLiteral( "FORMAT" ), 1015 QStringLiteral( "OUTPUTFORMAT" ), 1016 QStringLiteral( "BBOX" ), 1017 QStringLiteral( "FEATUREID" ), 1018 QStringLiteral( "TYPENAME" ), 1019 QStringLiteral( "FILTER" ), 1020 QStringLiteral( "EXP_FILTER" ), 1021 QStringLiteral( "MAXFEATURES" ), 1022 QStringLiteral( "STARTINDEX" ), 1023 QStringLiteral( "PROPERTYNAME" ), 1024 QStringLiteral( "_DC" ) 1025 }; 1026 1027 hitGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int numberOfFeatures,const QStringList & typeNames,const QgsServerSettings * settings)1028 void hitGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format, 1029 int numberOfFeatures, const QStringList &typeNames, const QgsServerSettings *settings ) 1030 { 1031 QDateTime now = QDateTime::currentDateTime(); 1032 QString fcString; 1033 1034 if ( format == QgsWfsParameters::Format::GeoJSON ) 1035 { 1036 response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" ); 1037 fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); 1038 fcString += QStringLiteral( " \"timeStamp\": \"%1\"\n" ).arg( now.toString( Qt::ISODate ) ); 1039 fcString += QStringLiteral( " \"numberOfFeatures\": %1\n" ).arg( QString::number( numberOfFeatures ) ); 1040 fcString += QLatin1Char( '}' ); 1041 } 1042 else 1043 { 1044 if ( format == QgsWfsParameters::Format::GML2 ) 1045 response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" ); 1046 else 1047 response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" ); 1048 1049 //Prepare url 1050 QString hrefString = serviceUrl( request, project, *settings ); 1051 1052 QUrl mapUrl( hrefString ); 1053 1054 QUrlQuery query( mapUrl ); 1055 query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) ); 1056 //Set version 1057 if ( mWfsParameters.version().isEmpty() ) 1058 query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() ); 1059 else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1060 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) ); 1061 else 1062 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) ); 1063 1064 for ( auto param : query.queryItems() ) 1065 { 1066 if ( sParamFilter.contains( param.first.toUpper() ) ) 1067 query.removeAllQueryItems( param.first ); 1068 } 1069 1070 query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) ); 1071 query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) ); 1072 if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1073 { 1074 if ( format == QgsWfsParameters::Format::GML2 ) 1075 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) ); 1076 else 1077 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) ); 1078 } 1079 else 1080 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) ); 1081 1082 mapUrl.setQuery( query ); 1083 1084 hrefString = mapUrl.toString(); 1085 1086 QString wfsSchema; 1087 if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1088 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" ); 1089 else 1090 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" ); 1091 1092 //wfs:FeatureCollection valid 1093 fcString = QStringLiteral( "<wfs:FeatureCollection" ); 1094 fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\""; 1095 fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\""; 1096 fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\""; 1097 fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" ); 1098 fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" ); 1099 fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\""; 1100 fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" ); 1101 fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&" ) ) + "\""; 1102 fcString += "\n timeStamp=\"" + now.toString( Qt::ISODate ) + "\""; 1103 fcString += "\n numberOfFeatures=\"" + QString::number( numberOfFeatures ) + "\""; 1104 fcString += QLatin1String( ">\n" ); 1105 fcString += QLatin1String( "</wfs:FeatureCollection>" ); 1106 } 1107 1108 response.write( fcString.toUtf8() ); 1109 response.flush(); 1110 } 1111 startGetFeature(const QgsServerRequest & request,QgsServerResponse & response,const QgsProject * project,QgsWfsParameters::Format format,int prec,QgsCoordinateReferenceSystem & crs,QgsRectangle * rect,const QStringList & typeNames,const QgsServerSettings * settings)1112 void startGetFeature( const QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project, QgsWfsParameters::Format format, 1113 int prec, QgsCoordinateReferenceSystem &crs, QgsRectangle *rect, const QStringList &typeNames, const QgsServerSettings *settings ) 1114 { 1115 QString fcString; 1116 1117 std::unique_ptr< QgsRectangle > transformedRect; 1118 1119 if ( format == QgsWfsParameters::Format::GeoJSON ) 1120 { 1121 response.setHeader( "Content-Type", "application/vnd.geo+json; charset=utf-8" ); 1122 1123 if ( crs.isValid() && !rect->isEmpty() ) 1124 { 1125 QgsGeometry exportGeom = QgsGeometry::fromRect( *rect ); 1126 QgsCoordinateTransform transform; 1127 transform.setSourceCrs( crs ); 1128 transform.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); 1129 try 1130 { 1131 if ( exportGeom.transform( transform ) == Qgis::GeometryOperationResult::Success ) 1132 { 1133 transformedRect.reset( new QgsRectangle( exportGeom.boundingBox() ) ); 1134 rect = transformedRect.get(); 1135 } 1136 } 1137 catch ( QgsException &cse ) 1138 { 1139 Q_UNUSED( cse ) 1140 } 1141 } 1142 // EPSG:4326 max extent is -180, -90, 180, 90 1143 rect = new QgsRectangle( rect->intersect( QgsRectangle( -180.0, -90.0, 180.0, 90.0 ) ) ); 1144 1145 fcString = QStringLiteral( "{\"type\": \"FeatureCollection\",\n" ); 1146 fcString += " \"bbox\": [ " + qgsDoubleToString( rect->xMinimum(), prec ) + ", " + qgsDoubleToString( rect->yMinimum(), prec ) + ", " + qgsDoubleToString( rect->xMaximum(), prec ) + ", " + qgsDoubleToString( rect->yMaximum(), prec ) + "],\n"; 1147 fcString += QLatin1String( " \"features\": [\n" ); 1148 response.write( fcString.toUtf8() ); 1149 } 1150 else 1151 { 1152 if ( format == QgsWfsParameters::Format::GML2 ) 1153 response.setHeader( "Content-Type", "text/xml; subtype=gml/2.1.2; charset=utf-8" ); 1154 else 1155 response.setHeader( "Content-Type", "text/xml; subtype=gml/3.1.1; charset=utf-8" ); 1156 1157 //Prepare url 1158 QString hrefString = serviceUrl( request, project, *settings ); 1159 1160 QUrl mapUrl( hrefString ); 1161 1162 QUrlQuery query( mapUrl ); 1163 query.addQueryItem( QStringLiteral( "SERVICE" ), QStringLiteral( "WFS" ) ); 1164 //Set version 1165 if ( mWfsParameters.version().isEmpty() ) 1166 query.addQueryItem( QStringLiteral( "VERSION" ), implementationVersion() ); 1167 else if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1168 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.1.0" ) ); 1169 else 1170 query.addQueryItem( QStringLiteral( "VERSION" ), QStringLiteral( "1.0.0" ) ); 1171 1172 const auto queryItems {query.queryItems()}; 1173 for ( auto param : std::as_const( queryItems ) ) 1174 { 1175 if ( sParamFilter.contains( param.first.toUpper() ) ) 1176 query.removeAllQueryItems( param.first ); 1177 } 1178 1179 query.addQueryItem( QStringLiteral( "REQUEST" ), QStringLiteral( "DescribeFeatureType" ) ); 1180 query.addQueryItem( QStringLiteral( "TYPENAME" ), typeNames.join( ',' ) ); 1181 if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1182 { 1183 if ( format == QgsWfsParameters::Format::GML2 ) 1184 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/2.1.2" ) ); 1185 else 1186 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "text/xml; subtype=gml/3.1.1" ) ); 1187 } 1188 else 1189 query.addQueryItem( QStringLiteral( "OUTPUTFORMAT" ), QStringLiteral( "XMLSCHEMA" ) ); 1190 1191 mapUrl.setQuery( query ); 1192 1193 hrefString = mapUrl.toString(); 1194 1195 QString wfsSchema; 1196 if ( mWfsParameters.version().isEmpty() || mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1197 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.1.0/wfs.xsd" ); 1198 else 1199 wfsSchema = QStringLiteral( "http://schemas.opengis.net/wfs/1.0.0/wfs.xsd" ); 1200 1201 //wfs:FeatureCollection valid 1202 fcString = QStringLiteral( "<wfs:FeatureCollection" ); 1203 fcString += " xmlns:wfs=\"" + WFS_NAMESPACE + "\""; 1204 fcString += " xmlns:ogc=\"" + OGC_NAMESPACE + "\""; 1205 fcString += " xmlns:gml=\"" + GML_NAMESPACE + "\""; 1206 fcString += QLatin1String( " xmlns:ows=\"http://www.opengis.net/ows\"" ); 1207 fcString += QLatin1String( " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" ); 1208 fcString += " xmlns:qgs=\"" + QGS_NAMESPACE + "\""; 1209 fcString += QLatin1String( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" ); 1210 fcString += " xsi:schemaLocation=\"" + WFS_NAMESPACE + " " + wfsSchema + " " + QGS_NAMESPACE + " " + hrefString.replace( QLatin1String( "&" ), QLatin1String( "&" ) ) + "\""; 1211 fcString += QLatin1String( ">\n" ); 1212 1213 response.write( fcString.toUtf8() ); 1214 response.flush(); 1215 1216 QDomDocument doc; 1217 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1218 if ( format == QgsWfsParameters::Format::GML3 ) 1219 { 1220 // For WFS 1.1 we honor requested CRS and axis order 1221 const QString srsName {request.serverParameters().value( QStringLiteral( "SRSNAME" ) )}; 1222 const bool invertAxis { mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) && 1223 crs.hasAxisInverted() && 1224 ! srsName.startsWith( QLatin1String( "EPSG:" ) ) }; 1225 QDomElement envElem = QgsOgcUtils::rectangleToGMLEnvelope( rect, doc, srsName, invertAxis, prec ); 1226 if ( !envElem.isNull() ) 1227 { 1228 if ( crs.isValid() ) 1229 { 1230 if ( mWfsParameters.versionAsNumber() >= QgsProjectVersion( 1, 1, 0 ) ) 1231 { 1232 envElem.setAttribute( QStringLiteral( "srsName" ), srsName ); 1233 } 1234 else 1235 { 1236 envElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1237 } 1238 } 1239 bbElem.appendChild( envElem ); 1240 doc.appendChild( bbElem ); 1241 } 1242 } 1243 else 1244 { 1245 QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( rect, doc, prec ); 1246 if ( !boxElem.isNull() ) 1247 { 1248 if ( crs.isValid() ) 1249 { 1250 boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1251 } 1252 bbElem.appendChild( boxElem ); 1253 doc.appendChild( bbElem ); 1254 } 1255 } 1256 response.write( doc.toByteArray() ); 1257 response.flush(); 1258 } 1259 } 1260 setGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format,const QgsFeature & feature,int featIdx,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1261 void setGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format, const QgsFeature &feature, int featIdx, 1262 const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1263 { 1264 if ( !feature.isValid() ) 1265 return; 1266 1267 if ( format == QgsWfsParameters::Format::GeoJSON ) 1268 { 1269 QString fcString; 1270 if ( featIdx == 0 ) 1271 fcString += QLatin1String( " " ); 1272 else 1273 fcString += QLatin1String( " ," ); 1274 mJsonExporter.setSourceCrs( params.crs ); 1275 mJsonExporter.setIncludeGeometry( false ); 1276 mJsonExporter.setIncludeAttributes( !params.attributeIndexes.isEmpty() ); 1277 mJsonExporter.setAttributes( params.attributeIndexes ); 1278 fcString += createFeatureGeoJSON( feature, params, pkAttributes ); 1279 fcString += QLatin1String( "\n" ); 1280 1281 response.write( fcString.toUtf8() ); 1282 } 1283 else 1284 { 1285 QDomDocument gmlDoc; 1286 QDomElement featureElement; 1287 if ( format == QgsWfsParameters::Format::GML3 ) 1288 { 1289 featureElement = createFeatureGML3( feature, gmlDoc, params, project, pkAttributes ); 1290 gmlDoc.appendChild( featureElement ); 1291 } 1292 else 1293 { 1294 featureElement = createFeatureGML2( feature, gmlDoc, params, project, pkAttributes ); 1295 gmlDoc.appendChild( featureElement ); 1296 } 1297 response.write( gmlDoc.toByteArray() ); 1298 } 1299 1300 // Stream partial content 1301 response.flush(); 1302 } 1303 endGetFeature(QgsServerResponse & response,QgsWfsParameters::Format format)1304 void endGetFeature( QgsServerResponse &response, QgsWfsParameters::Format format ) 1305 { 1306 QString fcString; 1307 if ( format == QgsWfsParameters::Format::GeoJSON ) 1308 { 1309 fcString += QLatin1String( " ]\n" ); 1310 fcString += QLatin1Char( '}' ); 1311 } 1312 else 1313 { 1314 fcString = QStringLiteral( "</wfs:FeatureCollection>\n" ); 1315 } 1316 response.write( fcString.toUtf8() ); 1317 } 1318 1319 createFeatureGeoJSON(const QgsFeature & feature,const createFeatureParams & params,const QgsAttributeList & pkAttributes)1320 QString createFeatureGeoJSON( const QgsFeature &feature, const createFeatureParams ¶ms, const QgsAttributeList &pkAttributes ) 1321 { 1322 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1323 //QgsJsonExporter force transform geometry to EPSG:4326 1324 //and the RFC 7946 GeoJSON specification recommends limiting coordinate precision to 6 1325 //Q_UNUSED( prec ) 1326 1327 //copy feature so we can modify its geometry as required 1328 QgsFeature f( feature ); 1329 QgsGeometry geom = feature.geometry(); 1330 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1331 { 1332 mJsonExporter.setIncludeGeometry( true ); 1333 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1334 { 1335 QgsRectangle box = geom.boundingBox(); 1336 f.setGeometry( QgsGeometry::fromRect( box ) ); 1337 } 1338 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1339 { 1340 f.setGeometry( geom.centroid() ); 1341 } 1342 } 1343 1344 return mJsonExporter.exportFeature( f, QVariantMap(), id ); 1345 } 1346 1347 createFeatureGML2(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1348 QDomElement createFeatureGML2( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1349 { 1350 //gml:FeatureMember 1351 QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); 1352 1353 //qgs:%TYPENAME% 1354 QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ ); 1355 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1356 typeNameElement.setAttribute( QStringLiteral( "fid" ), id ); 1357 featureElement.appendChild( typeNameElement ); 1358 1359 //add geometry column (as gml) 1360 QgsGeometry geom = feature.geometry(); 1361 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1362 { 1363 int prec = params.precision; 1364 QgsCoordinateReferenceSystem crs = params.crs; 1365 QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); 1366 try 1367 { 1368 QgsGeometry transformed = geom; 1369 if ( transformed.transform( mTransform ) == Qgis::GeometryOperationResult::Success ) 1370 { 1371 geom = transformed; 1372 crs = params.outputCrs; 1373 if ( crs.isGeographic() && !params.crs.isGeographic() ) 1374 prec = std::min( params.precision + 3, 6 ); 1375 } 1376 } 1377 catch ( QgsCsException &cse ) 1378 { 1379 Q_UNUSED( cse ) 1380 } 1381 1382 QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) ); 1383 QDomElement gmlElem; 1384 QgsGeometry cloneGeom( geom ); 1385 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1386 { 1387 cloneGeom = QgsGeometry::fromRect( geom.boundingBox() ); 1388 } 1389 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1390 { 1391 cloneGeom = geom.centroid(); 1392 } 1393 else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) ) 1394 { 1395 cloneGeom.convertToMultiType(); 1396 } 1397 const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet(); 1398 if ( abstractGeom ) 1399 { 1400 gmlElem = abstractGeom->asGml2( doc, prec, "http://www.opengis.net/gml" ); 1401 } 1402 1403 if ( !gmlElem.isNull() ) 1404 { 1405 QgsRectangle box = geom.boundingBox(); 1406 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1407 QDomElement boxElem = QgsOgcUtils::rectangleToGMLBox( &box, doc, prec ); 1408 1409 if ( crs.isValid() ) 1410 { 1411 boxElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1412 gmlElem.setAttribute( QStringLiteral( "srsName" ), crs.authid() ); 1413 } 1414 1415 bbElem.appendChild( boxElem ); 1416 typeNameElement.appendChild( bbElem ); 1417 1418 geomElem.appendChild( gmlElem ); 1419 typeNameElement.appendChild( geomElem ); 1420 } 1421 } 1422 1423 //read all attribute values from the feature 1424 QgsAttributes featureAttributes = feature.attributes(); 1425 QgsFields fields = feature.fields(); 1426 for ( int i = 0; i < params.attributeIndexes.count(); ++i ) 1427 { 1428 int idx = params.attributeIndexes[i]; 1429 if ( idx >= fields.count() ) 1430 { 1431 continue; 1432 } 1433 const QgsField field = fields.at( idx ); 1434 const QgsEditorWidgetSetup setup = field.editorWidgetSetup(); 1435 QString attributeName = field.name(); 1436 1437 QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 1438 QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) ); 1439 if ( featureAttributes[idx].isNull() ) 1440 { 1441 fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) ); 1442 } 1443 fieldElem.appendChild( fieldText ); 1444 typeNameElement.appendChild( fieldElem ); 1445 } 1446 1447 return featureElement; 1448 } 1449 createFeatureGML3(const QgsFeature & feature,QDomDocument & doc,const createFeatureParams & params,const QgsProject * project,const QgsAttributeList & pkAttributes)1450 QDomElement createFeatureGML3( const QgsFeature &feature, QDomDocument &doc, const createFeatureParams ¶ms, const QgsProject *project, const QgsAttributeList &pkAttributes ) 1451 { 1452 //gml:FeatureMember 1453 QDomElement featureElement = doc.createElement( QStringLiteral( "gml:featureMember" )/*wfs:FeatureMember*/ ); 1454 1455 //qgs:%TYPENAME% 1456 QDomElement typeNameElement = doc.createElement( "qgs:" + params.typeName /*qgs:%TYPENAME%*/ ); 1457 QString id = QStringLiteral( "%1.%2" ).arg( params.typeName, QgsServerFeatureId::getServerFid( feature, pkAttributes ) ); 1458 typeNameElement.setAttribute( QStringLiteral( "gml:id" ), id ); 1459 featureElement.appendChild( typeNameElement ); 1460 1461 //add geometry column (as gml) 1462 QgsGeometry geom = feature.geometry(); 1463 if ( !geom.isNull() && params.withGeom && params.geometryName != QLatin1String( "NONE" ) ) 1464 { 1465 int prec = params.precision; 1466 QgsCoordinateReferenceSystem crs = params.crs; 1467 QgsCoordinateTransform mTransform( crs, params.outputCrs, project ); 1468 try 1469 { 1470 QgsGeometry transformed = geom; 1471 if ( transformed.transform( mTransform ) == Qgis::GeometryOperationResult::Success ) 1472 { 1473 geom = transformed; 1474 crs = params.outputCrs; 1475 if ( crs.isGeographic() && !params.crs.isGeographic() ) 1476 prec = std::min( params.precision + 3, 6 ); 1477 } 1478 } 1479 catch ( QgsCsException &cse ) 1480 { 1481 Q_UNUSED( cse ) 1482 } 1483 1484 QDomElement geomElem = doc.createElement( QStringLiteral( "qgs:geometry" ) ); 1485 QDomElement gmlElem; 1486 QgsGeometry cloneGeom( geom ); 1487 if ( params.geometryName == QLatin1String( "EXTENT" ) ) 1488 { 1489 cloneGeom = QgsGeometry::fromRect( geom.boundingBox() ); 1490 } 1491 else if ( params.geometryName == QLatin1String( "CENTROID" ) ) 1492 { 1493 cloneGeom = geom.centroid(); 1494 } 1495 else if ( params.forceGeomToMulti && ! QgsWkbTypes::isMultiType( geom.wkbType() ) ) 1496 { 1497 cloneGeom.convertToMultiType(); 1498 } 1499 const QgsAbstractGeometry *abstractGeom = cloneGeom.constGet(); 1500 if ( abstractGeom ) 1501 { 1502 gmlElem = abstractGeom->asGml3( doc, prec, "http://www.opengis.net/gml", params.hasAxisInverted ? QgsAbstractGeometry::AxisOrder::YX : QgsAbstractGeometry::AxisOrder::XY ); 1503 } 1504 1505 if ( !gmlElem.isNull() ) 1506 { 1507 QgsRectangle box = geom.boundingBox(); 1508 QDomElement bbElem = doc.createElement( QStringLiteral( "gml:boundedBy" ) ); 1509 QDomElement boxElem = QgsOgcUtils::rectangleToGMLEnvelope( &box, doc, params.srsName, params.hasAxisInverted, prec ); 1510 1511 if ( crs.isValid() ) 1512 { 1513 boxElem.setAttribute( QStringLiteral( "srsName" ), params.srsName ); 1514 gmlElem.setAttribute( QStringLiteral( "srsName" ), params.srsName ); 1515 } 1516 1517 bbElem.appendChild( boxElem ); 1518 typeNameElement.appendChild( bbElem ); 1519 1520 geomElem.appendChild( gmlElem ); 1521 typeNameElement.appendChild( geomElem ); 1522 } 1523 } 1524 1525 //read all attribute values from the feature 1526 QgsAttributes featureAttributes = feature.attributes(); 1527 QgsFields fields = feature.fields(); 1528 for ( int i = 0; i < params.attributeIndexes.count(); ++i ) 1529 { 1530 int idx = params.attributeIndexes[i]; 1531 if ( idx >= fields.count() ) 1532 { 1533 continue; 1534 } 1535 1536 const QgsField field = fields.at( idx ); 1537 const QgsEditorWidgetSetup setup = field.editorWidgetSetup(); 1538 1539 QString attributeName = field.name(); 1540 1541 QDomElement fieldElem = doc.createElement( "qgs:" + attributeName.replace( ' ', '_' ).replace( cleanTagNameRegExp, QString() ) ); 1542 QDomText fieldText = doc.createTextNode( encodeValueToText( featureAttributes[idx], setup ) ); 1543 if ( featureAttributes[idx].isNull() ) 1544 { 1545 fieldElem.setAttribute( QStringLiteral( "xsi:nil" ), QStringLiteral( "true" ) ); 1546 } 1547 fieldElem.appendChild( fieldText ); 1548 typeNameElement.appendChild( fieldElem ); 1549 } 1550 1551 return featureElement; 1552 } 1553 encodeValueToText(const QVariant & value,const QgsEditorWidgetSetup & setup)1554 QString encodeValueToText( const QVariant &value, const QgsEditorWidgetSetup &setup ) 1555 { 1556 if ( value.isNull() ) 1557 return QString(); 1558 1559 if ( setup.type() == QStringLiteral( "DateTime" ) ) 1560 { 1561 QgsDateTimeFieldFormatter fieldFormatter; 1562 const QVariantMap config = setup.config(); 1563 const QString fieldFormat = config.value( QStringLiteral( "field_format" ), fieldFormatter.defaultFormat( value.type() ) ).toString(); 1564 QDateTime date = value.toDateTime(); 1565 1566 if ( date.isValid() ) 1567 { 1568 return date.toString( fieldFormat ); 1569 } 1570 } 1571 else if ( setup.type() == QStringLiteral( "Range" ) ) 1572 { 1573 const QVariantMap config = setup.config(); 1574 if ( config.contains( QStringLiteral( "Precision" ) ) ) 1575 { 1576 // if precision is defined, use it 1577 bool ok; 1578 int precision( config[ QStringLiteral( "Precision" ) ].toInt( &ok ) ); 1579 if ( ok ) 1580 return QString::number( value.toDouble(), 'f', precision ); 1581 } 1582 } 1583 1584 switch ( value.type() ) 1585 { 1586 case QVariant::Int: 1587 case QVariant::UInt: 1588 case QVariant::LongLong: 1589 case QVariant::ULongLong: 1590 case QVariant::Double: 1591 return value.toString(); 1592 1593 case QVariant::Bool: 1594 return value.toBool() ? QStringLiteral( "true" ) : QStringLiteral( "false" ); 1595 1596 case QVariant::StringList: 1597 case QVariant::List: 1598 case QVariant::Map: 1599 { 1600 QString v = QgsJsonUtils::encodeValue( value ); 1601 1602 //do we need CDATA 1603 if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 ) 1604 v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) ); 1605 1606 return v; 1607 } 1608 1609 default: 1610 case QVariant::String: 1611 { 1612 QString v = value.toString(); 1613 1614 //do we need CDATA 1615 if ( v.indexOf( '<' ) != -1 || v.indexOf( '&' ) != -1 ) 1616 v.prepend( QStringLiteral( "<![CDATA[" ) ).append( QStringLiteral( "]]>" ) ); 1617 1618 return v; 1619 } 1620 } 1621 } 1622 1623 1624 } // namespace 1625 1626 } // namespace QgsWfs 1627 1628 1629 1630