From 9bd23b5ac3ec3e52bda9835f836dd1ada364caaf Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 20 Jul 2018 11:00:52 +0200 Subject: [PATCH] [Server][Feature][needs-docs] Server Cache can be manage by plugins First commit to add a way to manage the QGIS Server cache with plugins. In this commit only GetCapabilities document can be cached by plugins. --- .../auto_generated/qgsserverinterface.sip.in | 13 ++ python/server/server_auto.sip | 6 + src/server/CMakeLists.txt | 2 + src/server/qgsservercachefilter.cpp | 56 +++++++ src/server/qgsservercachefilter.h | 93 +++++++++++ src/server/qgsservercachemanager.cpp | 72 ++++++++ src/server/qgsservercachemanager.h | 111 +++++++++++++ src/server/qgsserverinterface.h | 14 ++ src/server/qgsserverinterfaceimpl.cpp | 13 ++ src/server/qgsserverinterfaceimpl.h | 13 +- .../services/wcs/qgswcsgetcapabilities.cpp | 37 ++++- .../services/wfs/qgswfsgetcapabilities.cpp | 37 ++++- .../wfs/qgswfsgetcapabilities_1_0_0.cpp | 37 ++++- .../services/wms/qgswmsgetcapabilities.cpp | 39 ++++- tests/src/python/CMakeLists.txt | 1 + .../src/python/test_qgsserver_cachemanager.py | 154 ++++++++++++++++++ 16 files changed, 685 insertions(+), 13 deletions(-) create mode 100644 src/server/qgsservercachefilter.cpp create mode 100644 src/server/qgsservercachefilter.h create mode 100644 src/server/qgsservercachemanager.cpp create mode 100644 src/server/qgsservercachemanager.h create mode 100644 tests/src/python/test_qgsserver_cachemanager.py diff --git a/python/server/auto_generated/qgsserverinterface.sip.in b/python/server/auto_generated/qgsserverinterface.sip.in index 2146213ead9..a14c4d67fb3 100644 --- a/python/server/auto_generated/qgsserverinterface.sip.in +++ b/python/server/auto_generated/qgsserverinterface.sip.in @@ -83,6 +83,19 @@ Register an access control filter virtual QgsAccessControl *accessControls() const = 0; %Docstring Gets the registered access control filters +%End + + virtual void registerServerCache( QgsServerCacheFilter *serverCache /Transfer/, int priority = 0 ) = 0; +%Docstring +Register a server cache filter + +:param serverCache: the server cache to register +:param priority: the priority used to order them +%End + + virtual QgsServerCacheManager *cacheManager() const = 0; +%Docstring +Gets the registered server cache filters %End virtual QString getEnv( const QString &name ) const = 0; diff --git a/python/server/server_auto.sip b/python/server/server_auto.sip index 7c6a879dc9c..248908e5049 100644 --- a/python/server/server_auto.sip +++ b/python/server/server_auto.sip @@ -29,3 +29,9 @@ %If ( HAVE_SERVER_PYTHON_PLUGINS ) %Include auto_generated/qgsaccesscontrol.sip %End +%If ( HAVE_SERVER_PYTHON_PLUGINS ) +%Include auto_generated/qgsservercachefilter.sip +%End +%If ( HAVE_SERVER_PYTHON_PLUGINS ) +%Include auto_generated/qgsservercachemanager.sip +%End diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 7722404a945..247d2dcf429 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -73,6 +73,8 @@ IF (WITH_SERVER_PLUGINS) qgsserverfilter.cpp qgsaccesscontrolfilter.cpp qgsaccesscontrol.cpp + qgsservercachefilter.cpp + qgsservercachemanager.cpp ) ENDIF (WITH_SERVER_PLUGINS) diff --git a/src/server/qgsservercachefilter.cpp b/src/server/qgsservercachefilter.cpp new file mode 100644 index 00000000000..fe6e8d63238 --- /dev/null +++ b/src/server/qgsservercachefilter.cpp @@ -0,0 +1,56 @@ +/*************************************************************************** + qgsservercachefilter.cpp + ------------------------ + Cache interface for Qgis Server plugins + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsservercachefilter.h" + +#include + +//! Constructor +QgsServerCacheFilter::QgsServerCacheFilter( const QgsServerInterface *serverInterface ): + mServerInterface( serverInterface ) +{ +} + +//! Returns cached document +QByteArray QgsServerCacheFilter::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return QByteArray(); +} + +//! Updates or inserts the document in cache +bool QgsServerCacheFilter::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( doc ); + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} + +//! Deletes the cached document +bool QgsServerCacheFilter::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + Q_UNUSED( project ); + Q_UNUSED( request ); + Q_UNUSED( key ); + return false; +} diff --git a/src/server/qgsservercachefilter.h b/src/server/qgsservercachefilter.h new file mode 100644 index 00000000000..fab907db606 --- /dev/null +++ b/src/server/qgsservercachefilter.h @@ -0,0 +1,93 @@ +/*************************************************************************** + qgsservercachefilter.h + ------------------------ + Cache interface for Qgis Server plugins + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSERVERCACHEPLUGIN_H +#define QGSSERVERCACHEPLUGIN_H + +#include +#include +#include +#include "qgsproject.h" +#include "qgsserverrequest.h" +#include "qgis_server.h" +#include "qgis_sip.h" + +SIP_IF_MODULE( HAVE_SERVER_PYTHON_PLUGINS ) + +class QgsServerInterface; + + +/** + * \ingroup server + * \class QgsServerCacheFilter + * \brief Class defining cache interface for QGIS Server plugins. + */ +class SERVER_EXPORT QgsServerCacheFilter +{ + + public: + + /** + * Constructor + * QgsServerInterface passed to plugins constructors + * and must be passed to QgsServerCacheFilter instances. + */ + QgsServerCacheFilter( const QgsServerInterface *serverInterface ); + + virtual ~QgsServerCacheFilter() = default; + + /** + * Returns cached document (or 0 if document not in cache) like capabilities + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns QByteArray of the cached document or an empty one if no corresponding document found + */ + virtual QByteArray getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the document in cache like capabilities + * \param doc the document to cache + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been cached + */ + virtual bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached document + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been deleted + */ + virtual bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + private: + + //! The server interface + const QgsServerInterface *mServerInterface = nullptr; + +}; + +//! The registry definition +typedef QMultiMap QgsServerCacheFilterMap; + +#endif // QGSSERVERSECURITY_H diff --git a/src/server/qgsservercachemanager.cpp b/src/server/qgsservercachemanager.cpp new file mode 100644 index 00000000000..f044f161d6b --- /dev/null +++ b/src/server/qgsservercachemanager.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + qgsservercachemanager.cpp + ------------------------- + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsservercachemanager.h" + +//! Returns cached document (or 0 if document not in cache) like capabilities +const QDomDocument *QgsServerCacheManager::getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + QByteArray content = scIterator.value()->getCachedDocument( project, request, key ); + if ( !content.isEmpty() ) + { + QDomDocument doc; + if ( doc.setContent( content ) ) + { + return &doc; + } + } + } + return nullptr; +} + +//! Updates or inserts the document in cache like capabilities +bool QgsServerCacheManager::setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->setCachedDocument( doc, project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Deletes the cached document +bool QgsServerCacheManager::deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const +{ + QgsServerCacheFilterMap::const_iterator scIterator; + for ( scIterator = mPluginsServerCaches->constBegin(); scIterator != mPluginsServerCaches->constEnd(); ++scIterator ) + { + if ( scIterator.value()->deleteCachedDocument( project, request, key ) ) + { + return true; + } + } + return false; +} + +//! Register a new access control filter +void QgsServerCacheManager::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) +{ + mPluginsServerCaches->insert( priority, serverCache ); +} diff --git a/src/server/qgsservercachemanager.h b/src/server/qgsservercachemanager.h new file mode 100644 index 00000000000..60912ae277f --- /dev/null +++ b/src/server/qgsservercachemanager.h @@ -0,0 +1,111 @@ +/*************************************************************************** + qgsservercachemanager.h + ----------------------- + + begin : 2018-07-05 + copyright : (C) 2018 by René-Luc D'Hont + email : rldhont at 3liz dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSSERVERCACHEMANAGER_H +#define QGSSERVERCACHEMANAGER_H + +#include "qgsservercachefilter.h" +#include "qgsserverrequest.h" + +#include +#include +#include "qgsproject.h" +#include "qgis_server.h" +#include "qgis_sip.h" + +SIP_IF_MODULE( HAVE_SERVER_PYTHON_PLUGINS ) + +class QgsServerCachePlugin; + + +/** + * \ingroup server + * \class QgsServerCacheManager + * \brief A helper class that centralizes caches accesses given by all the server cache filter plugins. + * \since QGIS 3.4 + */ +class SERVER_EXPORT QgsServerCacheManager +{ +#ifdef SIP_RUN +#include "qgsservercachefilter.h" +#endif + + public: + //! Constructor + QgsServerCacheManager() + { + mPluginsServerCaches = new QgsServerCacheFilterMap(); + mResolved = false; + } + + //! Constructor + QgsServerCacheManager( const QgsServerCacheManager © ) + { + mPluginsServerCaches = new QgsServerCacheFilterMap( *copy.mPluginsServerCaches ); + mResolved = copy.mResolved; + } + + + ~QgsServerCacheManager() + { + delete mPluginsServerCaches; + } + + /** + * Returns cached document (or 0 if document not in cache) like capabilities + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns the cached document or 0 if no corresponding document found + */ + const QDomDocument *getCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Updates or inserts the document in cache like capabilities + * \param doc the document to cache + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been cached + */ + bool setCachedDocument( const QDomDocument *doc, const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Deletes the cached document + * \param project the project used to generate the document to provide path + * \param request the request used to generate the document to provider parameters or data + * \param key the key provided by the access control to identify differents documents for the same request + * \returns true if the document has been deleted + */ + bool deleteCachedDocument( const QgsProject *project, const QgsServerRequest &request, const QString &key ) const; + + /** + * Register a server cache filter + * \param serverCache the server cache to add + * \param priority the priority used to define the order + */ + void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ); + + private: + //! The ServerCache plugins registry + QgsServerCacheFilterMap *mPluginsServerCaches = nullptr; + + bool mResolved; +}; + +#endif diff --git a/src/server/qgsserverinterface.h b/src/server/qgsserverinterface.h index 03aa0cc54bf..a2bfee8ebae 100644 --- a/src/server/qgsserverinterface.h +++ b/src/server/qgsserverinterface.h @@ -30,9 +30,13 @@ #ifdef HAVE_SERVER_PYTHON_PLUGINS #include "qgsaccesscontrolfilter.h" #include "qgsaccesscontrol.h" +#include "qgsservercachefilter.h" +#include "qgsservercachemanager.h" #else class QgsAccessControl; class QgsAccessControlFilter; +class QgsServerCacheManager; +class QgsServerCacheFilter; #endif #include "qgsserviceregistry.h" #include "qgis_server.h" @@ -118,6 +122,16 @@ class SERVER_EXPORT QgsServerInterface //! Gets the registered access control filters virtual QgsAccessControl *accessControls() const = 0; + /** + * Register a server cache filter + * \param serverCache the server cache to register + * \param priority the priority used to order them + */ + virtual void registerServerCache( QgsServerCacheFilter *serverCache SIP_TRANSFER, int priority = 0 ) = 0; + + //! Gets the registered server cache filters + virtual QgsServerCacheManager *cacheManager() const = 0; + //! Returns an enrironment variable, used to pass environment variables to Python virtual QString getEnv( const QString &name ) const = 0; diff --git a/src/server/qgsserverinterfaceimpl.cpp b/src/server/qgsserverinterfaceimpl.cpp index de7832a105d..ddbcdb8622a 100644 --- a/src/server/qgsserverinterfaceimpl.cpp +++ b/src/server/qgsserverinterfaceimpl.cpp @@ -29,8 +29,10 @@ QgsServerInterfaceImpl::QgsServerInterfaceImpl( QgsCapabilitiesCache *capCache, mRequestHandler = nullptr; #ifdef HAVE_SERVER_PYTHON_PLUGINS mAccessControls = new QgsAccessControl(); + mCacheManager = new QgsServerCacheManager(); #else mAccessControls = nullptr; + mCacheManager = nullptr; #endif } @@ -84,6 +86,17 @@ void QgsServerInterfaceImpl::registerAccessControl( QgsAccessControlFilter *acce #endif } +//! Register a new access control filter +void QgsServerInterfaceImpl::registerServerCache( QgsServerCacheFilter *serverCache, int priority ) +{ +#ifdef HAVE_SERVER_PYTHON_PLUGINS + mCacheManager->registerServerCache( serverCache, priority ); +#else + Q_UNUSED( serverCache ); + Q_UNUSED( priority ); +#endif +} + void QgsServerInterfaceImpl::removeConfigCacheEntry( const QString &path ) { diff --git a/src/server/qgsserverinterfaceimpl.h b/src/server/qgsserverinterfaceimpl.h index 39f19b4ff4f..a8a7e4b973f 100644 --- a/src/server/qgsserverinterfaceimpl.h +++ b/src/server/qgsserverinterfaceimpl.h @@ -50,8 +50,8 @@ class QgsServerInterfaceImpl : public QgsServerInterface QgsRequestHandler *requestHandler() override { return mRequestHandler; } void registerFilter( QgsServerFilter *filter, int priority = 0 ) override; QgsServerFiltersMap filters() override { return mFilters; } + //! Register an access control filter - // void registerAccessControl( QgsAccessControlFilter *accessControl, int priority = 0 ) override; /** @@ -59,6 +59,16 @@ class QgsServerInterfaceImpl : public QgsServerInterface * \returns the access control helper */ QgsAccessControl *accessControls() const override { return mAccessControls; } + + //! Register a server cache filter + void registerServerCache( QgsServerCacheFilter *serverCache, int priority = 0 ) override; + + /** + * Gets the helper over all the registered server cache filters + * \returns the server cache helper + */ + QgsServerCacheManager *cacheManager() const override { return mCacheManager; } + QString getEnv( const QString &name ) const override; QString configFilePath() override { return mConfigFilePath; } void setConfigFilePath( const QString &configFilePath ) override; @@ -74,6 +84,7 @@ class QgsServerInterfaceImpl : public QgsServerInterface QString mConfigFilePath; QgsServerFiltersMap mFilters; QgsAccessControl *mAccessControls = nullptr; + QgsServerCacheManager *mCacheManager = nullptr; QgsCapabilitiesCache *mCapabilitiesCache = nullptr; QgsRequestHandler *mRequestHandler = nullptr; QgsServiceRegistry *mServiceRegistry = nullptr; diff --git a/src/server/services/wcs/qgswcsgetcapabilities.cpp b/src/server/services/wcs/qgswcsgetcapabilities.cpp index 1020205da28..aa760ec1c37 100644 --- a/src/server/services/wcs/qgswcsgetcapabilities.cpp +++ b/src/server/services/wcs/qgswcsgetcapabilities.cpp @@ -37,10 +37,43 @@ namespace QgsWcs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities.cpp b/src/server/services/wfs/qgswfsgetcapabilities.cpp index df63ed486a5..a628e49c1b5 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities.cpp @@ -41,10 +41,43 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp index 77423fcfd6f..a771b8dc622 100644 --- a/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp +++ b/src/server/services/wfs/qgswfsgetcapabilities_1_0_0.cpp @@ -43,10 +43,43 @@ namespace QgsWfs void writeGetCapabilities( QgsServerInterface *serverIface, const QgsProject *project, const QString &version, const QgsServerRequest &request, QgsServerResponse &response ) { - QDomDocument doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + QStringList cacheKeyList; + bool cache = true; + + QgsAccessControl *accessControl = serverIface->accessControls(); + if ( accessControl ) + cache = accessControl->fillCacheKey( cacheKeyList ); + + QDomDocument doc; + QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one + { + doc = createGetCapabilitiesDocument( serverIface, project, version, request ); + + if ( cache && cacheManager ) + { + if ( cacheManager->setCachedDocument( &doc, project, request, cacheKey ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKey ); + } + } + if ( !capabilitiesDocument ) + { + doc = doc.cloneNode().toDocument(); + capabilitiesDocument = &doc; + } + } response.setHeader( "Content-Type", "text/xml; charset=utf-8" ); - response.write( doc.toByteArray() ); + response.write( capabilitiesDocument->toByteArray() ); } diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 0785ed82c85..c3fedd687c8 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -100,15 +100,26 @@ namespace QgsWms cacheKeyList << request.url().host(); bool cache = true; -#ifdef HAVE_SERVER_PYTHON_PLUGINS QgsAccessControl *accessControl = serverIface->accessControls(); if ( accessControl ) cache = accessControl->fillCacheKey( cacheKeyList ); -#endif + QDomDocument doc; QString cacheKey = cacheKeyList.join( QStringLiteral( "-" ) ); - const QDomDocument *capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + const QDomDocument *capabilitiesDocument; + + QgsServerCacheManager *cacheManager = serverIface->cacheManager(); + if ( cacheManager && cache ) + { + if ( cacheKeyList.count() == 2 ) + capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + else if ( cacheKeyList.count() > 2 ) + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + } + + if ( !capabilitiesDocument ) //capabilities xml not in cache plugins + capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); if ( !capabilitiesDocument ) //capabilities xml not in cache. Create a new one { QgsMessageLog::logMessage( QStringLiteral( "Capabilities document not found in cache" ) ); @@ -117,10 +128,26 @@ namespace QgsWms if ( cache ) { - capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); - capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + if ( cacheManager ) + { + if ( cacheKeyList.count() == 2 && + cacheManager->setCachedDocument( &doc, project, request, QStringLiteral( "" ) ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, QStringLiteral( "" ) ); + } + else if ( cacheKeyList.count() > 2 && + cacheManager->setCachedDocument( &doc, project, request, cacheKeyList.at( 3 ) ) ) + { + capabilitiesDocument = cacheManager->getCachedDocument( project, request, cacheKeyList.at( 3 ) ); + } + } + else + { + capabilitiesCache->insertCapabilitiesDocument( configFilePath, cacheKey, &doc ); + capabilitiesDocument = capabilitiesCache->searchCapabilitiesDocument( configFilePath, cacheKey ); + } } - else + if ( !capabilitiesDocument ) { doc = doc.cloneNode().toDocument(); capabilitiesDocument = &doc; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index bab9627d80b..f198c7e4834 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -268,6 +268,7 @@ IF (WITH_SERVER) ADD_PYTHON_TEST(PyQgsServerAccessControlWFS test_qgsserver_accesscontrol_wfs.py) ADD_PYTHON_TEST(PyQgsServerAccessControlWCS test_qgsserver_accesscontrol_wcs.py) ADD_PYTHON_TEST(PyQgsServerAccessControlWFSTransactional test_qgsserver_accesscontrol_wfs_transactional.py) + ADD_PYTHON_TEST(PyQgsServerCacheManager test_qgsserver_cachemanager.py) ADD_PYTHON_TEST(PyQgsServerWFS test_qgsserver_wfs.py) ADD_PYTHON_TEST(PyQgsServerWFST test_qgsserver_wfst.py) ADD_PYTHON_TEST(PyQgsOfflineEditingWFS test_offline_editing_wfs.py) diff --git a/tests/src/python/test_qgsserver_cachemanager.py b/tests/src/python/test_qgsserver_cachemanager.py new file mode 100644 index 00000000000..3f245ca66fb --- /dev/null +++ b/tests/src/python/test_qgsserver_cachemanager.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsServer. + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'René-Luc DHONT' +__date__ = '19/07/2018' +__copyright__ = 'Copyright 2015, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +print('CTEST_FULL_OUTPUT') + +import qgis # NOQA + +import os +import urllib.request +import urllib.parse +import urllib.error +import tempfile +import hashlib + +from qgis.testing import unittest +from utilities import unitTestDataPath +from qgis.server import QgsServer, QgsServerCacheFilter, QgsServerRequest, QgsBufferServerRequest, QgsBufferServerResponse +from qgis.core import QgsApplication, QgsFontUtils +from qgis.PyQt.QtCore import QFile, QByteArray +from qgis.PyQt.QtXml import QDomDocument + + +class PyServerCache(QgsServerCacheFilter): + + """ Used to have restriction access """ + + # Be able to deactivate the access control to have a reference point + _active = False + + def __init__(self, server_iface): + super(QgsServerCacheFilter, self).__init__(server_iface) + self._cache_dir = os.path.join(tempfile.gettempdir(), "qgs_server_cache") + if not os.path.exists(self._cache_dir): + os.mkdir(self._cache_dir) + + def getCachedDocument(self, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + + if not os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")): + return QByteArray() + + doc = QDomDocument(m.hexdigest() + ".xml") + with open(os.path.join(self._cache_dir, m.hexdigest() + ".xml"), "r") as f: + statusOK, errorStr, errorLine, errorColumn = doc.setContent(f.read(), True) + if not statusOK: + print("Could not read or find the contents document. Error at line %d, column %d:\n%s" % (errorLine, errorColumn, errorStr)) + return QByteArray() + + return doc.toByteArray() + + def setCachedDocument(self, doc, project, request, key): + m = hashlib.md5() + paramMap = request.parameters() + urlParam = "&".join(["%s=%s" % (k, paramMap[k]) for k in paramMap.keys()]) + m.update(urlParam.encode('utf8')) + with open(os.path.join(self._cache_dir, m.hexdigest() + ".xml"), "w") as f: + f.write(doc.toString()) + return os.path.exists(os.path.join(self._cache_dir, m.hexdigest() + ".xml")) + + +class TestQgsServerCacheManager(unittest.TestCase): + + @classmethod + def _handle_request(cls, qs, requestMethod=QgsServerRequest.GetMethod, data=None): + if data is not None: + data = data.encode('utf-8') + request = QgsBufferServerRequest(qs, requestMethod, {}, data) + response = QgsBufferServerResponse() + cls._server.handleRequest(request, response) + headers = [] + rh = response.headers() + rk = sorted(rh.keys()) + for k in rk: + headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) + return b"\n".join(headers) + b"\n\n", bytes(response.body()) + + @classmethod + def setUpClass(cls): + """Run before all tests""" + cls._app = QgsApplication([], False) + cls._server = QgsServer() + cls._handle_request("") + cls._server_iface = cls._server.serverInterface() + cls._servercache = PyServerCache(cls._server_iface) + cls._server_iface.registerServerCache(cls._servercache, 100) + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + filelist = [f for f in os.listdir(cls._servercache._cache_dir) if f.endswith(".xml")] + for f in filelist: + os.remove(os.path.join(cls._servercache._cache_dir, f)) + del cls._server + cls._app.exitQgis + + def _result(self, data): + headers = {} + for line in data[0].decode('UTF-8').split("\n"): + if line != "": + header = line.split(":") + self.assertEqual(len(header), 2, line) + headers[str(header[0])] = str(header[1]).strip() + + return data[1], headers + + def _execute_request(self, qs, requestMethod=QgsServerRequest.GetMethod, data=None): + request = QgsBufferServerRequest(qs, requestMethod, {}, data) + response = QgsBufferServerResponse() + self._server.handleRequest(request, response) + headers = [] + rh = response.headers() + rk = sorted(rh.keys()) + for k in rk: + headers.append(("%s: %s" % (k, rh[k])).encode('utf-8')) + return b"\n".join(headers) + b"\n\n", bytes(response.body()) + + def setUp(self): + """Create the server instance""" + self.fontFamily = QgsFontUtils.standardTestFontFamily() + QgsFontUtils.loadStandardTestFonts(['All']) + + d = unitTestDataPath('qgis_server_accesscontrol') + '/' + self._project_path = os.path.join(d, "project.qgs") + + def test_getcapabilities(self): + project = self._project_path + assert os.path.exists(project), "Project file not found: " + project + + query_string = '?MAP=%s&SERVICE=WMS&VERSION=1.3.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + query_string = '?MAP=%s&SERVICE=WFS&VERSION=1.1.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + query_string = '?MAP=%s&SERVICE=WCS&VERSION=1.0.0&REQUEST=%s' % (urllib.parse.quote(project), 'GetCapabilities') + header, body = self._execute_request(query_string) + + +if __name__ == "__main__": + unittest.main()