1 /***************************************************************************
2     qgswfsshareddata.cpp
3     ---------------------
4     begin                : March 2016
5     copyright            : (C) 2016 by Even Rouault
6     email                : even.rouault at spatialys.com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include "qgswfsshareddata.h"
17 #include "qgswfsutils.h"
18 #include "qgscachedirectorymanager.h"
19 #include "qgsogcutils.h"
20 #include "qgsexpression.h"
21 #include "qgsmessagelog.h"
22 #include "qgslogger.h"
23 #include <QUrlQuery>
24 
QgsWFSSharedData(const QString & uri)25 QgsWFSSharedData::QgsWFSSharedData( const QString &uri )
26   : QgsBackgroundCachedSharedData( "wfs", tr( "WFS" ) )
27   , mURI( uri )
28 {
29   mHideProgressDialog = mURI.hideDownloadProgressDialog();
30   mServerPrefersCoordinatesForTransactions_1_1 = mURI.preferCoordinatesForWfst11();
31 }
32 
~QgsWFSSharedData()33 QgsWFSSharedData::~QgsWFSSharedData()
34 {
35   QgsDebugMsgLevel( QStringLiteral( "~QgsWFSSharedData()" ), 4 );
36 
37   cleanup();
38 }
39 
newFeatureDownloaderImpl(QgsFeatureDownloader * downloader,bool requestMadeFromMainThread)40 std::unique_ptr<QgsFeatureDownloaderImpl> QgsWFSSharedData::newFeatureDownloaderImpl( QgsFeatureDownloader *downloader, bool requestMadeFromMainThread )
41 {
42   return std::unique_ptr<QgsFeatureDownloaderImpl>( new QgsWFSFeatureDownloaderImpl( this, downloader, requestMadeFromMainThread ) );
43 }
44 
isRestrictedToRequestBBOX() const45 bool QgsWFSSharedData::isRestrictedToRequestBBOX() const
46 {
47   return mURI.isRestrictedToRequestBBOX();
48 }
49 
invalidateCacheBaseUnderLock()50 void QgsWFSSharedData::invalidateCacheBaseUnderLock()
51 {
52 }
53 
54 
srsName() const55 QString QgsWFSSharedData::srsName() const
56 {
57   QString srsName;
58   if ( !mSourceCrs.authid().isEmpty() )
59   {
60     if ( mWFSVersion.startsWith( QLatin1String( "1.0" ) ) ||
61          !mSourceCrs.authid().startsWith( QLatin1String( "EPSG:" ) ) ||
62          // For servers like Geomedia that advertise EPSG:XXXX in capabilities even in WFS 1.1 or 2.0
63          mCaps.useEPSGColumnFormat )
64     {
65       srsName = mSourceCrs.authid();
66     }
67     else
68     {
69       QStringList list = mSourceCrs.authid().split( ':' );
70       srsName = QStringLiteral( "urn:ogc:def:crs:EPSG::%1" ).arg( list.last() );
71     }
72   }
73   return srsName;
74 }
75 
computeFilter(QString & errorMsg)76 bool QgsWFSSharedData::computeFilter( QString &errorMsg )
77 {
78   errorMsg.clear();
79   mWFSFilter.clear();
80   mSortBy.clear();
81 
82   QgsOgcUtils::GMLVersion gmlVersion;
83   QgsOgcUtils::FilterVersion filterVersion;
84   bool honourAxisOrientation = false;
85   if ( mWFSVersion.startsWith( QLatin1String( "1.0" ) ) )
86   {
87     gmlVersion = QgsOgcUtils::GML_2_1_2;
88     filterVersion = QgsOgcUtils::FILTER_OGC_1_0;
89   }
90   else if ( mWFSVersion.startsWith( QLatin1String( "1.1" ) ) )
91   {
92     honourAxisOrientation = !mURI.ignoreAxisOrientation();
93     gmlVersion = QgsOgcUtils::GML_3_1_0;
94     filterVersion = QgsOgcUtils::FILTER_OGC_1_1;
95   }
96   else
97   {
98     honourAxisOrientation = !mURI.ignoreAxisOrientation();
99     gmlVersion = QgsOgcUtils::GML_3_2_1;
100     filterVersion = QgsOgcUtils::FILTER_FES_2_0;
101   }
102 
103   if ( !mURI.sql().isEmpty() )
104   {
105     const QgsSQLStatement sql( mURI.sql() );
106 
107     const QgsSQLStatement::NodeSelect *select = dynamic_cast<const QgsSQLStatement::NodeSelect *>( sql.rootNode() );
108     if ( !select )
109     {
110       // Makes Coverity happy, but cannot happen in practice
111       QgsDebugMsg( QStringLiteral( "should not happen" ) );
112       return false;
113     }
114     const QList<QgsSQLStatement::NodeColumnSorted *> orderBy = select->orderBy();
115     const auto constOrderBy = orderBy;
116     for ( QgsSQLStatement::NodeColumnSorted *columnSorted : constOrderBy )
117     {
118       if ( !mSortBy.isEmpty() )
119         mSortBy += QLatin1Char( ',' );
120       mSortBy += columnSorted->column()->name();
121       if ( !columnSorted->ascending() )
122       {
123         if ( mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
124           mSortBy += QLatin1String( " DESC" );
125         else
126           mSortBy += QLatin1String( " D" );
127       }
128     }
129 
130     QDomDocument filterDoc;
131     const QDomElement filterElem = QgsOgcUtils::SQLStatementToOgcFilter(
132                                      sql, filterDoc, gmlVersion, filterVersion, mLayerPropertiesList,
133                                      honourAxisOrientation, mURI.invertAxisOrientation(),
134                                      mCaps.mapUnprefixedTypenameToPrefixedTypename,
135                                      &errorMsg );
136     if ( !errorMsg.isEmpty() )
137     {
138       errorMsg = tr( "SQL statement to OGC Filter error: " ) + errorMsg;
139       return false;
140     }
141     if ( !filterElem.isNull() )
142     {
143       filterDoc.appendChild( filterElem );
144       mWFSFilter = filterDoc.toString();
145     }
146   }
147   else
148   {
149     const QString filter( mURI.filter() );
150     if ( !filter.isEmpty() )
151     {
152       //test if filterString is already an OGC filter xml
153       QDomDocument filterDoc;
154       if ( filterDoc.setContent( filter ) )
155       {
156         mWFSFilter = filter;
157       }
158       else
159       {
160         //if not, if must be a QGIS expression
161         const QgsExpression filterExpression( filter );
162 
163         const QDomElement filterElem = QgsOgcUtils::expressionToOgcFilter(
164                                          filterExpression, filterDoc, gmlVersion, filterVersion, mGeometryAttribute,
165                                          srsName(), honourAxisOrientation, mURI.invertAxisOrientation(),
166                                          &errorMsg );
167 
168         if ( !errorMsg.isEmpty() )
169         {
170           errorMsg = tr( "Expression to OGC Filter error: " ) + errorMsg;
171           return false;
172         }
173         if ( !filterElem.isNull() )
174         {
175           filterDoc.appendChild( filterElem );
176           mWFSFilter = filterDoc.toString();
177         }
178       }
179     }
180   }
181 
182   return true;
183 }
184 
pushError(const QString & errorMsg) const185 void QgsWFSSharedData::pushError( const QString &errorMsg ) const
186 {
187   QgsMessageLog::logMessage( errorMsg, tr( "WFS" ) );
188   emit raiseError( errorMsg );
189 }
190 
createParser() const191 QgsGmlStreamingParser *QgsWFSSharedData::createParser() const
192 {
193   QgsGmlStreamingParser::AxisOrientationLogic axisOrientationLogic( QgsGmlStreamingParser::Honour_EPSG_if_urn );
194   if ( mURI.ignoreAxisOrientation() )
195   {
196     axisOrientationLogic = QgsGmlStreamingParser::Ignore_EPSG;
197   }
198 
199   if ( !mLayerPropertiesList.isEmpty() )
200   {
201     QList< QgsGmlStreamingParser::LayerProperties > layerPropertiesList;
202     const auto constMLayerPropertiesList = mLayerPropertiesList;
203     for ( const QgsOgcUtils::LayerProperties &layerProperties : constMLayerPropertiesList )
204     {
205       QgsGmlStreamingParser::LayerProperties layerPropertiesOut;
206       layerPropertiesOut.mName = layerProperties.mName;
207       layerPropertiesOut.mGeometryAttribute = layerProperties.mGeometryAttribute;
208       layerPropertiesList << layerPropertiesOut;
209     }
210 
211     return new QgsGmlStreamingParser( layerPropertiesList,
212                                       mFields,
213                                       mMapFieldNameToSrcLayerNameFieldName,
214                                       axisOrientationLogic,
215                                       mURI.invertAxisOrientation() );
216   }
217   else
218   {
219     return new QgsGmlStreamingParser( mURI.typeName(),
220                                       mGeometryAttribute,
221                                       mFields,
222                                       axisOrientationLogic,
223                                       mURI.invertAxisOrientation() );
224   }
225 }
226 
227 
getExtentFromSingleFeatureRequest() const228 QgsRectangle QgsWFSSharedData::getExtentFromSingleFeatureRequest() const
229 {
230   QgsWFSSingleFeatureRequest request( this );
231   return request.getExtent();
232 }
233 
getFeatureCountFromServer() const234 long long QgsWFSSharedData::getFeatureCountFromServer() const
235 {
236   QgsWFSFeatureHitsRequest request( mURI );
237   return request.getFeatureCount( mWFSVersion, mWFSFilter, mCaps );
238 }
239 
detectPotentialServerAxisOrderIssueFromSingleFeatureExtent() const240 bool QgsWFSSharedData::detectPotentialServerAxisOrderIssueFromSingleFeatureExtent() const
241 {
242   Q_ASSERT( !mComputedExtent.isNull() );
243   if ( mWFSVersion.startsWith( QLatin1String( "1.1" ) ) &&
244        !mURI.ignoreAxisOrientation() &&
245        !mURI.invertAxisOrientation() &&
246        mSourceCrs.hasAxisInverted() &&
247        mCapabilityExtent.contains( mComputedExtent ) )
248   {
249     pushError( QObject::tr( "It is likely that there is an issue with coordinate axis order of geometries when interacting with the server. You may want to enable the Ignore axis orientation and/or Invert axis orientation settings of the WFS connection." ) );
250     return true;
251   }
252   return false;
253 }
254 
255 // -------------------------
256 
257 
QgsWFSFeatureHitsRequest(const QgsWFSDataSourceURI & uri)258 QgsWFSFeatureHitsRequest::QgsWFSFeatureHitsRequest( const QgsWFSDataSourceURI &uri )
259   : QgsWfsRequest( uri )
260 {
261 }
262 
getFeatureCount(const QString & WFSVersion,const QString & filter,const QgsWfsCapabilities::Capabilities & caps)263 long long QgsWFSFeatureHitsRequest::getFeatureCount( const QString &WFSVersion,
264     const QString &filter, const QgsWfsCapabilities::Capabilities &caps )
265 {
266   const QString typeName = mUri.typeName();
267 
268   QUrl getFeatureUrl( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
269   QUrlQuery query( getFeatureUrl );
270   query.addQueryItem( QStringLiteral( "VERSION" ),  WFSVersion );
271   if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
272   {
273     query.addQueryItem( QStringLiteral( "TYPENAMES" ), typeName );
274   }
275   else
276   {
277     query.addQueryItem( QStringLiteral( "TYPENAME" ), typeName );
278   }
279 
280   const QString namespaceValue( caps.getNamespaceParameterValue( WFSVersion, typeName ) );
281   if ( !namespaceValue.isEmpty() )
282   {
283     if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
284     {
285       query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
286     }
287     else
288     {
289       query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
290     }
291   }
292 
293   if ( !filter.isEmpty() )
294   {
295     query.addQueryItem( QStringLiteral( "FILTER" ), filter );
296   }
297   query.addQueryItem( QStringLiteral( "RESULTTYPE" ), QStringLiteral( "hits" ) );
298 
299   getFeatureUrl.setQuery( query );
300   if ( !sendGET( getFeatureUrl, QString(), true ) )
301     return -1;
302 
303   const QByteArray &buffer = response();
304 
305   QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSFeatureHitsRequest: " ) + buffer, 4 );
306 
307   // parse XML
308   QString error;
309   QDomDocument domDoc;
310   if ( !domDoc.setContent( buffer, true, &error ) )
311   {
312     QgsDebugMsg( QStringLiteral( "parsing failed: " ) + error );
313     return -1;
314   }
315 
316   const QDomElement doc = domDoc.documentElement();
317   const QString numberOfFeatures =
318     ( WFSVersion.startsWith( QLatin1String( "1.1" ) ) ) ? doc.attribute( QStringLiteral( "numberOfFeatures" ) ) :
319     /* 2.0 */                         doc.attribute( QStringLiteral( "numberMatched" ) );
320   if ( !numberOfFeatures.isEmpty() )
321   {
322     bool isValid;
323     const long long ret = numberOfFeatures.toLongLong( &isValid );
324     if ( !isValid )
325       return -1;
326     return ret;
327   }
328 
329   return -1;
330 }
331 
errorMessageWithReason(const QString & reason)332 QString QgsWFSFeatureHitsRequest::errorMessageWithReason( const QString &reason )
333 {
334   return tr( "Download of feature count failed: %1" ).arg( reason );
335 }
336 
337 
338 // -------------------------
339 
340 
QgsWFSSingleFeatureRequest(const QgsWFSSharedData * shared)341 QgsWFSSingleFeatureRequest::QgsWFSSingleFeatureRequest( const QgsWFSSharedData *shared )
342   : QgsWfsRequest( shared->mURI ), mShared( shared )
343 {
344 }
345 
getExtent()346 QgsRectangle QgsWFSSingleFeatureRequest::getExtent()
347 {
348   QUrl getFeatureUrl( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
349   QUrlQuery query( getFeatureUrl );
350   query.addQueryItem( QStringLiteral( "VERSION" ),  mShared->mWFSVersion );
351   if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
352     query.addQueryItem( QStringLiteral( "TYPENAMES" ), mUri.typeName() );
353   else
354     query.addQueryItem( QStringLiteral( "TYPENAME" ), mUri.typeName() );
355 
356   const QString namespaceValue( mShared->mCaps.getNamespaceParameterValue( mShared->mWFSVersion, mUri.typeName() ) );
357   if ( !namespaceValue.isEmpty() )
358   {
359     if ( mShared->mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
360       query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
361     else
362       query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
363   }
364 
365   if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
366     query.addQueryItem( QStringLiteral( "COUNT" ), QString::number( 1 ) );
367   else
368     query.addQueryItem( QStringLiteral( "MAXFEATURES" ), QString::number( 1 ) );
369 
370   getFeatureUrl.setQuery( query );
371   if ( !sendGET( getFeatureUrl, QString(), true ) )
372     return QgsRectangle();
373 
374   const QByteArray &buffer = response();
375 
376   QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSSingleFeatureRequest: " ) + buffer, 4 );
377 
378   // parse XML
379   QgsGmlStreamingParser *parser = mShared->createParser();
380   QString gmlProcessErrorMsg;
381   QgsRectangle extent;
382   if ( parser->processData( buffer, true, gmlProcessErrorMsg ) )
383   {
384     QVector<QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair> featurePtrList =
385       parser->getAndStealReadyFeatures();
386     for ( int i = 0; i < featurePtrList.size(); i++ )
387     {
388       const QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair &featPair = featurePtrList[i];
389       const QgsFeature f( *( featPair.first ) );
390       const QgsGeometry geometry = f.geometry();
391       if ( !geometry.isNull() )
392       {
393         extent = geometry.boundingBox();
394       }
395       delete featPair.first;
396     }
397   }
398   delete parser;
399   return extent;
400 }
401 
errorMessageWithReason(const QString & reason)402 QString QgsWFSSingleFeatureRequest::errorMessageWithReason( const QString &reason )
403 {
404   return tr( "Download of feature failed: %1" ).arg( reason );
405 }
406 
407