QGIS API Documentation  3.2.0-Bonn (bc43194)
qgsgeonoderequest.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsgeonoderequest.h
3  ---------------------
4  begin : Jul 2017
5  copyright : (C) 2017 by Muhammad Yarjuna Rohmat, Ismail Sunni
6  email : rohmat at kartoza dot com, ismail at kartoza dot 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 
17 #include "qgssettings.h"
18 #include "qgsmessagelog.h"
19 #include "qgslogger.h"
20 #include "qgsgeonoderequest.h"
21 
22 #include <QEventLoop>
23 #include <QNetworkCacheMetaData>
24 #include <QByteArray>
25 #include <QJsonDocument>
26 #include <QJsonObject>
27 #include <QUrl>
28 #include <QDomDocument>
29 
30 QgsGeoNodeRequest::QgsGeoNodeRequest( const QString &baseUrl, bool forceRefresh, QObject *parent )
31  : QObject( parent )
32  , mBaseUrl( baseUrl )
33  , mForceRefresh( forceRefresh )
34 {
35 
36 }
37 
39 {
40  abort();
41 }
42 
44 {
45  mIsAborted = true;
46  if ( mGeoNodeReply )
47  {
48  mGeoNodeReply->deleteLater();
49  mGeoNodeReply = nullptr;
50  }
51 }
52 
54 {
55  request( QStringLiteral( "/api/layers/" ) );
56  QObject *obj = new QObject( this );
57 
58  connect( this, &QgsGeoNodeRequest::requestFinished, obj, [obj, this ]
59  {
60  QList<QgsGeoNodeRequest::ServiceLayerDetail> layers;
61  if ( mError.isEmpty() )
62  {
63  layers = parseLayers( this->lastResponse() );
64  }
65  emit layersFetched( layers );
66 
67  obj->deleteLater();
68  } );
69 }
70 
71 QList<QgsGeoNodeRequest::ServiceLayerDetail> QgsGeoNodeRequest::fetchLayersBlocking()
72 {
73  QList<QgsGeoNodeRequest::ServiceLayerDetail> layers;
74 
75  QEventLoop loop;
76  connect( this, &QgsGeoNodeRequest::requestFinished, &loop, &QEventLoop::quit );
77  QObject *obj = new QObject( this );
78  connect( this, &QgsGeoNodeRequest::layersFetched, obj, [&]( const QList<QgsGeoNodeRequest::ServiceLayerDetail> &fetched )
79  {
80  layers = fetched;
81  } );
82  fetchLayers();
83  loop.exec( QEventLoop::ExcludeUserInputEvents );
84  delete obj;
85  return layers;
86 }
87 
89 {
90  QgsGeoNodeStyle defaultStyle;
91  bool success = requestBlocking( QStringLiteral( "/api/layers?name=" ) + layerName );
92  if ( !success )
93  {
94  return defaultStyle;
95  }
96 
97  const QJsonDocument jsonDocument = QJsonDocument::fromJson( this->lastResponse() );
98  const QJsonObject jsonObject = jsonDocument.object();
99  const QList<QVariant> layers = jsonObject.toVariantMap().value( QStringLiteral( "objects" ) ).toList();
100  if ( layers.count() < 1 )
101  {
102  return defaultStyle;
103  }
104  QString defaultStyleUrl = layers.at( 0 ).toMap().value( QStringLiteral( "default_style" ) ).toString();
105 
106  defaultStyle = retrieveStyle( defaultStyleUrl );
107 
108  return defaultStyle;
109 
110 }
111 
112 QList<QgsGeoNodeStyle> QgsGeoNodeRequest::fetchStylesBlocking( const QString &layerName )
113 {
114  QList<QgsGeoNodeStyle> geoNodeStyles;
115  bool success = requestBlocking( QStringLiteral( "/api/styles?layer__name=" ) + layerName );
116  if ( !success )
117  {
118  return geoNodeStyles;
119  }
120 
121  const QJsonDocument jsonDocument = QJsonDocument::fromJson( this->lastResponse() );
122  const QJsonObject jsobObject = jsonDocument.object();
123  const QList<QVariant> styles = jsobObject.toVariantMap().value( QStringLiteral( "objects" ) ).toList();
124 
125  for ( const QVariant &style : styles )
126  {
127  const QVariantMap styleMap = style.toMap();
128  QString styleUrl = styleMap.value( QStringLiteral( "resource_uri" ) ).toString();
129  QgsGeoNodeStyle geoNodeStyle = retrieveStyle( styleUrl );
130  if ( !geoNodeStyle.name.isEmpty() )
131  {
132  geoNodeStyles.append( geoNodeStyle );
133  }
134  }
135 
136  return geoNodeStyles;
137 
138 }
139 
141 {
142  QString endPoint = QStringLiteral( "/api/styles/" ) + styleId;
143 
144  return retrieveStyle( endPoint );
145 }
146 
147 void QgsGeoNodeRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
148 {
149  QString msg = tr( "%1 of %2 bytes of request downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) );
150  QgsDebugMsgLevel( msg, 3 );
151  emit statusChanged( msg );
152 }
153 
155 {
156  return mProtocol;
157 }
158 
160 {
161  mProtocol = protocol;
162 }
163 
164 void QgsGeoNodeRequest::replyFinished()
165 {
166  QgsDebugMsg( "Reply finished" );
167  if ( !mIsAborted && mGeoNodeReply )
168  {
169  if ( mGeoNodeReply->error() == QNetworkReply::NoError )
170  {
171  QgsDebugMsg( "reply OK" );
172  QVariant redirect = mGeoNodeReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
173  if ( !redirect.isNull() )
174  {
175 
176  emit statusChanged( QStringLiteral( "GeoNode request redirected." ) );
177 
178  const QUrl &toUrl = redirect.toUrl();
179  if ( toUrl == mGeoNodeReply->url() )
180  {
181  mError = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
182  QgsMessageLog::logMessage( mError, tr( "GeoNode" ) );
183  mHttpGeoNodeResponse.clear();
184  }
185  else
186  {
187  QNetworkRequest request( toUrl );
188 
189  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
190  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
191 
192  mGeoNodeReply->deleteLater();
193  mGeoNodeReply = nullptr;
194 
195  QgsDebugMsgLevel( QString( "redirected getcapabilities: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 3 );
196  mGeoNodeReply = QgsNetworkAccessManager::instance()->get( request );
197 
198  connect( mGeoNodeReply, &QNetworkReply::finished, this, &QgsGeoNodeRequest::replyFinished, Qt::DirectConnection );
199  connect( mGeoNodeReply, &QNetworkReply::downloadProgress, this, &QgsGeoNodeRequest::replyProgress, Qt::DirectConnection );
200  return;
201  }
202  }
203  else
204  {
206 
207  if ( nam->cache() )
208  {
209  QNetworkCacheMetaData cmd = nam->cache()->metaData( mGeoNodeReply->request().url() );
210 
211  QNetworkCacheMetaData::RawHeaderList hl;
212  const QNetworkCacheMetaData::RawHeaderList cmdHeaders = cmd.rawHeaders();
213  for ( const QNetworkCacheMetaData::RawHeader &h : cmdHeaders )
214  {
215  if ( h.first != QStringLiteral( "Cache-Control" ) )
216  hl.append( h );
217  }
218  cmd.setRawHeaders( hl );
219 
220  QgsDebugMsg( QString( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ) );
221  if ( cmd.expirationDate().isNull() )
222  {
223  QgsSettings settings;
224  cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( settings.value( QStringLiteral( "qgis/defaultCapabilitiesExpiry" ), "24", QgsSettings::Providers ).toInt() * 60 * 60 ) );
225  }
226 
227  nam->cache()->updateMetaData( cmd );
228  }
229  else
230  {
231  QgsDebugMsg( "No cache for capabilities!" );
232  }
233 
234  mHttpGeoNodeResponse = mGeoNodeReply->readAll();
235 
236  if ( mHttpGeoNodeResponse.isEmpty() )
237  {
238  mError = tr( "Empty capabilities: %1" ).arg( mGeoNodeReply->errorString() );
239  }
240  }
241  }
242  else
243  {
244  mError = tr( "Request failed: %1" ).arg( mGeoNodeReply->errorString() );
245  QgsMessageLog::logMessage( mError, tr( "GeoNode" ) );
246  mHttpGeoNodeResponse.clear();
247  }
248  }
249 
250  if ( mGeoNodeReply )
251  {
252  mGeoNodeReply->deleteLater();
253  mGeoNodeReply = nullptr;
254  }
255 
256  emit requestFinished();
257 }
258 
259 QList<QgsGeoNodeRequest::ServiceLayerDetail> QgsGeoNodeRequest::parseLayers( const QByteArray &layerResponse )
260 {
261  QList<QgsGeoNodeRequest::ServiceLayerDetail> layers;
262  if ( layerResponse.isEmpty() )
263  {
264  return layers;
265  }
266 
267  const QJsonDocument jsonDocument = QJsonDocument::fromJson( layerResponse );
268  const QJsonObject jsonObject = jsonDocument.object();
269  const QVariantMap jsonVariantMap = jsonObject.toVariantMap();
270  const QVariantList layerList = jsonVariantMap.value( QStringLiteral( "objects" ) ).toList();
271  qint16 majorVersion;
272  qint16 minorVersion;
273  if ( jsonVariantMap.contains( QStringLiteral( "geonode_version" ) ) )
274  {
275  const QStringList geonodeVersionSplit = jsonVariantMap.value( QStringLiteral( "geonode_version" ) ).toString().split( '.' );
276  majorVersion = geonodeVersionSplit.at( 0 ).toInt();
277  minorVersion = geonodeVersionSplit.at( 1 ).toInt();
278  }
279  else
280  {
281  majorVersion = 2;
282  minorVersion = 6;
283  }
284 
285  if ( majorVersion == 2 && minorVersion == 6 )
286  {
287  for ( const QVariant &layer : qgis::as_const( layerList ) )
288  {
290  const QVariantMap layerMap = layer.toMap();
291  // Find WMS and WFS. XYZ is not available
292  // Trick to get layer's typename from distribution_url or detail_url
293  QString layerTypeName = layerMap.value( QStringLiteral( "detail_url" ) ).toString().split( '/' ).last();
294  if ( layerTypeName.isEmpty() )
295  {
296  layerTypeName = layerMap.value( QStringLiteral( "distribution_url" ) ).toString().split( '/' ).last();
297  }
298  // On this step, layerTypeName is in WORKSPACE%3ALAYERNAME or WORKSPACE:LAYERNAME format
299  if ( layerTypeName.contains( QStringLiteral( "%3A" ) ) )
300  {
301  layerTypeName.replace( QStringLiteral( "%3A" ), QStringLiteral( ":" ) );
302  }
303  // On this step, layerTypeName is in WORKSPACE:LAYERNAME format
304  const QStringList splitURL = layerTypeName.split( ':' );
305  QString layerWorkspace = splitURL.at( 0 );
306  QString layerName = splitURL.at( 1 );
307 
308  layerStruct.name = layerName;
309  layerStruct.typeName = layerTypeName;
310  layerStruct.uuid = layerMap.value( QStringLiteral( "uuid" ) ).toString();
311  layerStruct.title = layerMap.value( QStringLiteral( "title" ) ).toString();
312 
313  // WMS url : BASE_URI/geoserver/WORKSPACE/wms
314  layerStruct.wmsURL = mBaseUrl + QStringLiteral( "/geoserver/" ) + layerWorkspace + QStringLiteral( "/wms" );
315  // WFS url : BASE_URI/geoserver/WORKSPACE/wfs
316  layerStruct.wfsURL = mBaseUrl + QStringLiteral( "/geoserver/" ) + layerWorkspace + QStringLiteral( "/wfs" );
317  // XYZ url : set to empty string
318  layerStruct.xyzURL.clear();
319 
320  layers.append( layerStruct );
321  }
322  }
323  // Geonode version 2.7 or newer
324  else if ( ( majorVersion == 2 && minorVersion >= 7 ) || ( majorVersion >= 3 ) )
325  {
326  for ( const QVariant &layer : qgis::as_const( layerList ) )
327  {
329  const QVariantMap layerMap = layer.toMap();
330  // Find WMS, WFS, and XYZ link
331  const QVariantList layerLinks = layerMap.value( QStringLiteral( "links" ) ).toList();
332  for ( const QVariant &link : layerLinks )
333  {
334  const QVariantMap linkMap = link.toMap();
335  if ( linkMap.contains( QStringLiteral( "link_type" ) ) )
336  {
337  if ( linkMap.value( QStringLiteral( "link_type" ) ) == QStringLiteral( "OGC:WMS" ) )
338  {
339  layerStruct.wmsURL = linkMap.value( QStringLiteral( "url" ) ).toString();
340  }
341  else if ( linkMap.value( QStringLiteral( "link_type" ) ) == QStringLiteral( "OGC:WFS" ) )
342  {
343  layerStruct.wfsURL = linkMap.value( QStringLiteral( "url" ) ).toString();
344  }
345  else if ( linkMap.value( QStringLiteral( "link_type" ) ) == QStringLiteral( "image" ) )
346  {
347  if ( linkMap.contains( QStringLiteral( "name" ) ) && linkMap.value( QStringLiteral( "name" ) ) == QStringLiteral( "Tiles" ) )
348  {
349  layerStruct.xyzURL = linkMap.value( QStringLiteral( "url" ) ).toString();
350  }
351  }
352  }
353  }
354  if ( layerMap.value( QStringLiteral( "typename" ) ).toString().isEmpty() )
355  {
356  const QStringList splitURL = layerMap.value( QStringLiteral( "detail_url" ) ).toString().split( '/' );
357  layerStruct.typeName = splitURL.at( splitURL.length() - 1 );
358  }
359  layerStruct.uuid = layerMap.value( QStringLiteral( "uuid" ) ).toString();
360  layerStruct.name = layerMap.value( QStringLiteral( "name" ) ).toString();
361  layerStruct.typeName = layerMap.value( QStringLiteral( "typename" ) ).toString();
362  layerStruct.title = layerMap.value( QStringLiteral( "title" ) ).toString();
363  layers.append( layerStruct );
364  }
365  }
366  return layers;
367 }
368 
369 QgsGeoNodeStyle QgsGeoNodeRequest::retrieveStyle( const QString &styleUrl )
370 {
371  QgsGeoNodeStyle geoNodeStyle;
372 
373  bool success = requestBlocking( styleUrl );
374  if ( !success )
375  {
376  return geoNodeStyle;
377  }
378  const QJsonDocument jsonDocument = QJsonDocument::fromJson( this->lastResponse() );
379  const QJsonObject jsonObject = jsonDocument.object();
380 
381  const QVariantMap jsonMap = jsonObject.toVariantMap();
382  geoNodeStyle.id = jsonMap.value( QStringLiteral( "id" ) ).toString();
383  geoNodeStyle.name = jsonMap.value( QStringLiteral( "name" ) ).toString();
384  geoNodeStyle.title = jsonMap.value( QStringLiteral( "title" ) ).toString();
385  geoNodeStyle.styleUrl = jsonMap.value( QStringLiteral( "style_url" ) ).toString();
386 
387  success = requestBlocking( geoNodeStyle.styleUrl );
388  if ( !success )
389  {
390  return geoNodeStyle;
391  }
392 
393  success = geoNodeStyle.body.setContent( this->lastResponse() );
394  if ( !success )
395  {
396  return geoNodeStyle;
397  }
398 
399  return geoNodeStyle;
400 }
401 
402 QStringList QgsGeoNodeRequest::fetchServiceUrlsBlocking( const QString &serviceType )
403 {
404  QStringList urls;
405 
406  const QList<QgsGeoNodeRequest::ServiceLayerDetail> layers = fetchLayersBlocking();
407 
408  if ( layers.empty() )
409  {
410  return urls;
411  }
412 
413  for ( const QgsGeoNodeRequest::ServiceLayerDetail &layer : layers )
414  {
415  QString url;
416  if ( QString::compare( serviceType, QStringLiteral( "wms" ), Qt::CaseInsensitive ) == 0 )
417  {
418  url = layer.wmsURL;
419  }
420  else if ( QString::compare( serviceType, QStringLiteral( "wfs" ), Qt::CaseInsensitive ) == 0 )
421  {
422  url = layer.wfsURL;
423  }
424  else if ( QString::compare( serviceType, QStringLiteral( "xyz" ), Qt::CaseInsensitive ) == 0 )
425  {
426  url = layer.xyzURL;
427  }
428 
429  if ( url.isEmpty() )
430  continue;
431 
432  if ( !url.contains( QLatin1String( "://" ) ) )
433  {
434  url.prepend( protocol() );
435  }
436  if ( !urls.contains( url ) )
437  {
438  urls.append( url );
439  }
440  }
441 
442  return urls;
443 }
444 
446 {
447  QgsStringMap urls;
448 
449  const QList<QgsGeoNodeRequest::ServiceLayerDetail> layers = fetchLayersBlocking();
450 
451  if ( layers.empty() )
452  {
453  return urls;
454  }
455 
456  for ( const QgsGeoNodeRequest::ServiceLayerDetail &layer : layers )
457  {
458  QString url;
459 
460  if ( QString::compare( serviceType, QStringLiteral( "wms" ), Qt::CaseInsensitive ) == 0 )
461  {
462  url = layer.wmsURL;
463  }
464  else if ( QString::compare( serviceType, QStringLiteral( "wfs" ), Qt::CaseInsensitive ) == 0 )
465  {
466  url = layer.wfsURL;
467  }
468  else if ( QString::compare( serviceType, QStringLiteral( "xyz" ), Qt::CaseInsensitive ) == 0 )
469  {
470  url = layer.xyzURL;
471  }
472 
473  if ( url.isEmpty() )
474  continue;
475 
476  QString layerName = layer.name;
477  if ( !url.contains( QLatin1String( "://" ) ) )
478  {
479  url.prepend( protocol() );
480  }
481  if ( !urls.contains( url ) )
482  {
483  urls.insert( layerName, url );
484  }
485  }
486 
487  return urls;
488 }
489 
490 void QgsGeoNodeRequest::request( const QString &endPoint )
491 {
492  abort();
493  mIsAborted = false;
494  // Handle case where the endpoint is full url
495  QString url = endPoint.startsWith( mBaseUrl ) ? endPoint : mBaseUrl + endPoint;
496  QgsDebugMsg( "Requesting to " + url );
497  setProtocol( url.split( QStringLiteral( "://" ) ).at( 0 ) );
498  QUrl layerUrl( url );
499  layerUrl.setScheme( protocol() );
500 
501  mError.clear();
502 
503  mGeoNodeReply = requestUrl( url );
504  connect( mGeoNodeReply, &QNetworkReply::finished, this, &QgsGeoNodeRequest::replyFinished, Qt::DirectConnection );
505  connect( mGeoNodeReply, &QNetworkReply::downloadProgress, this, &QgsGeoNodeRequest::replyProgress, Qt::DirectConnection );
506 }
507 
508 bool QgsGeoNodeRequest::requestBlocking( const QString &endPoint )
509 {
510  request( endPoint );
511 
512  QEventLoop loop;
513  connect( this, &QgsGeoNodeRequest::requestFinished, &loop, &QEventLoop::quit );
514  loop.exec( QEventLoop::ExcludeUserInputEvents );
515 
516  return mError.isEmpty();
517 }
518 
519 QNetworkReply *QgsGeoNodeRequest::requestUrl( const QString &url )
520 {
521  QNetworkRequest request( url );
522  // Add authentication check here
523 
524  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
525  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
526 
527  return QgsNetworkAccessManager::instance()->get( request );
528 }
529 
530 
void fetchLayers()
Triggers a new request to fetch the list of available layers from the server.
void layersFetched(const QList< QgsGeoNodeRequest::ServiceLayerDetail > &layers)
Emitted when the result of a fetchLayers call has been received and processed.
Encapsulates information about a GeoNode layer style.
void setProtocol(const QString &protocol)
Sets the network protocol (e.g.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
~QgsGeoNodeRequest() override
QString styleUrl
Associated URL.
Service layer details for an individual layer from a GeoNode connection.
QByteArray lastResponse() const
Returns the most recent response obtained from the server.
void request(const QString &endPoint)
Triggers a new request to the GeoNode server, with the requested endPoint.
QString wmsURL
WMS URL for layer.
QgsStringMap fetchServiceUrlDataBlocking(const QString &serviceType)
Obtains a map of layer name to URL for available services with matching serviceType from the server...
QList< QgsGeoNodeStyle > fetchStylesBlocking(const QString &layerName)
Requests the list of available styles for the layer with matching layerName from the server...
QMap< QString, QString > QgsStringMap
Definition: qgis.h:501
QDomDocument body
DOM documenting containing style.
void statusChanged(const QString &statusQString)
Emitted when the status of an ongoing request is changed.
QgsGeoNodeStyle fetchDefaultStyleBlocking(const QString &layerName)
Requests the default style for the layer with matching layerName from the server. ...
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
QString xyzURL
XYZ tileserver URL for layer.
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
QList< QgsGeoNodeRequest::ServiceLayerDetail > fetchLayersBlocking()
Requests the list of available layers from the server.
QString wfsURL
WFS URL for layer.
QgsGeoNodeRequest(const QString &baseUrl, bool forceRefresh, QObject *parent=nullptr)
Constructor for QgsGeoNodeRequest.
void requestFinished()
Emitted when the existing request has been completed.
QString id
Unique style ID.
QString protocol() const
Returns the network protocol (e.g.
QString name
Style name.
bool requestBlocking(const QString &endPoint)
Triggers a new request to the GeoNode server, with the requested endPoint.
QUuid uuid
Unique identifier (generate on the client side, not at the GeoNode server)
void abort()
Aborts any active network request immediately.
network access manager for QGISThis class implements the QGIS network access manager.
QgsGeoNodeStyle fetchStyleBlocking(const QString &styleId)
Requests the details for the style with matching styleId from the server.
QStringList fetchServiceUrlsBlocking(const QString &serviceType)
Requests the list of unique URLs for available services with matching serviceType from the server...
QString title
Style title.