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     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     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     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     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         QgsExpression filterExpression( filter );
162 
163         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 ( 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 int 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 int QgsWFSFeatureHitsRequest::getFeatureCount( const QString &WFSVersion,
264     const QString &filter, const QgsWfsCapabilities::Capabilities &caps )
265 {
266   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   query.addQueryItem( QStringLiteral( "TYPENAME" ), typeName );
276 
277   QString namespaceValue( caps.getNamespaceParameterValue( WFSVersion, typeName ) );
278   if ( !namespaceValue.isEmpty() )
279   {
280     if ( WFSVersion.startsWith( QLatin1String( "2.0" ) ) )
281       query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
282     query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
283   }
284 
285   if ( !filter.isEmpty() )
286   {
287     query.addQueryItem( QStringLiteral( "FILTER" ), filter );
288   }
289   query.addQueryItem( QStringLiteral( "RESULTTYPE" ), QStringLiteral( "hits" ) );
290 
291   getFeatureUrl.setQuery( query );
292   if ( !sendGET( getFeatureUrl, QString(), true ) )
293     return -1;
294 
295   const QByteArray &buffer = response();
296 
297   QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSFeatureHitsRequest: " ) + buffer, 4 );
298 
299   // parse XML
300   QString error;
301   QDomDocument domDoc;
302   if ( !domDoc.setContent( buffer, true, &error ) )
303   {
304     QgsDebugMsg( QStringLiteral( "parsing failed: " ) + error );
305     return -1;
306   }
307 
308   QDomElement doc = domDoc.documentElement();
309   QString numberOfFeatures =
310     ( WFSVersion.startsWith( QLatin1String( "1.1" ) ) ) ? doc.attribute( QStringLiteral( "numberOfFeatures" ) ) :
311     /* 2.0 */                         doc.attribute( QStringLiteral( "numberMatched" ) );
312   if ( !numberOfFeatures.isEmpty() )
313   {
314     bool isValid;
315     int ret = numberOfFeatures.toInt( &isValid );
316     if ( !isValid )
317       return -1;
318     return ret;
319   }
320 
321   return -1;
322 }
323 
errorMessageWithReason(const QString & reason)324 QString QgsWFSFeatureHitsRequest::errorMessageWithReason( const QString &reason )
325 {
326   return tr( "Download of feature count failed: %1" ).arg( reason );
327 }
328 
329 
330 // -------------------------
331 
332 
QgsWFSSingleFeatureRequest(const QgsWFSSharedData * shared)333 QgsWFSSingleFeatureRequest::QgsWFSSingleFeatureRequest( const QgsWFSSharedData *shared )
334   : QgsWfsRequest( shared->mURI ), mShared( shared )
335 {
336 }
337 
getExtent()338 QgsRectangle QgsWFSSingleFeatureRequest::getExtent()
339 {
340   QUrl getFeatureUrl( mUri.requestUrl( QStringLiteral( "GetFeature" ) ) );
341   QUrlQuery query( getFeatureUrl );
342   query.addQueryItem( QStringLiteral( "VERSION" ),  mShared->mWFSVersion );
343   if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
344     query.addQueryItem( QStringLiteral( "TYPENAMES" ), mUri.typeName() );
345   query.addQueryItem( QStringLiteral( "TYPENAME" ), mUri.typeName() );
346 
347   QString namespaceValue( mShared->mCaps.getNamespaceParameterValue( mShared->mWFSVersion, mUri.typeName() ) );
348   if ( !namespaceValue.isEmpty() )
349   {
350     if ( mShared->mWFSVersion.startsWith( QLatin1String( "2.0" ) ) )
351       query.addQueryItem( QStringLiteral( "NAMESPACES" ), namespaceValue );
352     query.addQueryItem( QStringLiteral( "NAMESPACE" ), namespaceValue );
353   }
354 
355   if ( mShared->mWFSVersion .startsWith( QLatin1String( "2.0" ) ) )
356     query.addQueryItem( QStringLiteral( "COUNT" ), QString::number( 1 ) );
357   else
358     query.addQueryItem( QStringLiteral( "MAXFEATURES" ), QString::number( 1 ) );
359 
360   getFeatureUrl.setQuery( query );
361   if ( !sendGET( getFeatureUrl, QString(), true ) )
362     return QgsRectangle();
363 
364   const QByteArray &buffer = response();
365 
366   QgsDebugMsgLevel( QStringLiteral( "parsing QgsWFSSingleFeatureRequest: " ) + buffer, 4 );
367 
368   // parse XML
369   QgsGmlStreamingParser *parser = mShared->createParser();
370   QString gmlProcessErrorMsg;
371   QgsRectangle extent;
372   if ( parser->processData( buffer, true, gmlProcessErrorMsg ) )
373   {
374     QVector<QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair> featurePtrList =
375       parser->getAndStealReadyFeatures();
376     for ( int i = 0; i < featurePtrList.size(); i++ )
377     {
378       QgsGmlStreamingParser::QgsGmlFeaturePtrGmlIdPair &featPair = featurePtrList[i];
379       QgsFeature f( *( featPair.first ) );
380       QgsGeometry geometry = f.geometry();
381       if ( !geometry.isNull() )
382       {
383         extent = geometry.boundingBox();
384       }
385       delete featPair.first;
386     }
387   }
388   delete parser;
389   return extent;
390 }
391 
errorMessageWithReason(const QString & reason)392 QString QgsWFSSingleFeatureRequest::errorMessageWithReason( const QString &reason )
393 {
394   return tr( "Download of feature failed: %1" ).arg( reason );
395 }
396 
397