Python plug-ins voor QGIS server

Plug-ins voor Python kunnen ook worden uitgevoerd op QGIS Server (zie: QGIS als OGC Data Server): door de server interface (QgsServerInterface) te gebruiken kan een plug-in voor Python die wordt uitgevoerd op de server het gedrag van bestaande bronservices (WMS, WFS etc.) wijzigen.

Met de server filter interface (QgsServerFilter) kunnen we de parameters voor de invoer wijzigen, de gegenereerde uitvoer wijzigen of zelfs nieuwe services verschaffen.

Met de access control interface (QgsAccessControlFilter) kunnen we enkele toegangsbeperkingen per verzoeken toepassen.

Server Filter Plugins architecture

Server plug-ins voor Python worden geladen als eenmaal de toepassing FCGI is gestart. Zij registreren één of meerdere QgsServerFilter (op dit punt is het misschien nuttig om even snel te kijken naar server plugins API docs). Elk filter zou ten minste één van drie terugkoppelingen moeten implementeren:

  • requestReady()
  • responseComplete()
  • sendResponse()

Alle filters hebben toegang tot het object voor het verzoek/antwoord (QgsRequestHandler) en kan al zijn eigenschappen bewerken (invoer/uitvoer) en exceptions opwerpen (hoewel op een bijzondere manier zoals we hieronder zullen zien).

Hier is een pseudocode die een typische serversessie weergeeft en wanneer de terugkoppelingen van het filter worden aangeroepen:

  • Haal het inkomende verzoek op
    • maak afhandeling GET/POST/SOAP voor het verzoek

    • geef verzoek door aan een instantie van QgsServerInterface

    • roep plug-ins requestReady() filters aan

    • indien er geen antwoord is
      • als SERVICE WMS/WFS/WCS is
        • maak WMS/WFS/WCS server
          • roep server’s executeRequest() aan en roep mogelijk aan sendResponse() plug-in filters bij stromende uitvoer of sla de byte stromende uitvoer en het type inhoud op in de afhandeling van het verzoek

      • roep plug-ins responseComplete() filters aan

    • roep plug-ins sendResponse() filters aan

    • afhandeling van het verzoek voert het antwoord uit

De volgende alinea’s beschrijven de beschikbare terugkoppelingen tot in detail.

requestReady

Dit wordt aangeroepen als het verzoek gereed is: inkomende URL en gegevens zijn geparset en vóór te schakelen naar de bronservices (WMS, WFS etc.), is dit het punt waar u de invoer kunt bewerken en acties kunt uitvoeren als:

  • authenticatie/autorisatie

  • doorverwijzingen

  • bepaalde parameters toevoegen/verwijderen (typenamen bijvoorbeeld)

  • exceptions opwerpen

U zou zelfs een bronservice volledig kunnen vervangen door de parameter SERVICE te wijzigen en op die manier de bronservice volledig omzeilen (niet dat dat echter enige zin zou hebben).

sendResponse

Deze wordt aangeroepen wanneer de uitvoer wordt verzonden aan FCGI stdout (en van daaruit naar de cliënt), dit wordt normaal gesproken gedaan nadat bronservices hun proces hebben voltooid en nadat hook responseComplete werd aangeroepen, maar in een klein aantal gevallen kan de XML zo groot worden dat een stromende XML implementatie nodig was (WFS GetFeature is één ervan), in dit geval werd sendResponse() meerdere keren aangeroepen voordat het antwoord volledig was (en vóórdat responseComplete() werd aangeroepen). De voor de hand liggende consequentie is dat sendResponse() normala gesproken eenmaal wordt aangeroepen maar zou bij uitzondering meerdere keren aangeroepen kunnen worden en in dat geval (en alleen in dat geval) wordt het ook aangeroepen vóór responseComplete().

sendResponse() is de beste plaats voor het direct bewerken van de uitvoer van bronservices en hoewel responseComplete() gewoonlijk ook een optie is, is sendResponse() de enige geldige optie in het geval van stromende services.

responseComplete

Dit wordt eenmaal aangeroepen wanneer de bronservices (indien aangesproken) hun proces voltooien en het verzoek gereed is om te worden verzonden naar de cliënt. Zoals hierboven besproken wordt dit normala gesproken aangeroepen vóór sendResponse() met uitzondering van stromende services (of andere filters voor plug-ins) die sendResponse() eerder zouden hebben kunnen aangeroepen.

responseComplete() is de ideale plek om implementatie voor nieuwe services te verschaffen (WPS of aangepaste services) en om de uitvoer, komende vanaf bronservices, direct te bewerken (bijvoorbeeld om ene watermerk aan een afbeelding van WMS toe te voegen).

Een uitzondering opwerpen vanuit een plug-in

Enig werk moet voor dit onderwerp nog worden gedaan: de huidige implementatie kan onderscheid maken tussen afgehandelde en niet afgehandelde uitzonderingen door het instellen van een eigenschap QgsRequestHandler voor een instantie van QgsMapServiceException, op deze manier kan de hoofdcode van C++ de afgehandelde uitzonderingen van Python afvangen en niet afgehandelde uitzonderingen negeren (of beter nog: ze loggen).

Deze benadering werkt in de basis maar is nog niet erg “Pythonisch”: een betere benadering zou zijn om uitzonderingen op te werpen vanuit de code van Python en ze op zien borrelen in een lus van C++ om daar te worden afgehandeld.

Een plug-in voor de server schrijven

Een plug-in voor de server is eenvoudigweg een standaard plug-in in Python voor QGIS Python zoals beschreven in Python plug-ins ontwikkelen, dat eenvoudigweg een aanvullende (of alternatieve) interface verschaft: ee typische plug-in voor QGIS Desktop heeft toegang tot de toepassing QGIS via de instantie QgisInterface, een plug-in voor de server heeft ook toegang tot een QgsServerInterface.

Een speciaal item voor metadata is nodig (in metadata.txt) om QGIS Server te vertellen dat een plug-in een interface voor de server heeft:

server=True

De hier besproken voorbeeldplug-in (met nog veel meer voorbeeldfilters) is beschikbaar op github: QGIS HelloServer Example Plugin

Plug-inbestanden

Hier is de mappenstructuur van onze voorbeeld-plug-in voor de server

PYTHON_PLUGINS_PATH/
  HelloServer/
    __init__.py    --> *required*
    HelloServer.py  --> *required*
    metadata.txt   --> *required*

__init__.py

Dit bestand wordt vereist door het systeem voor importeren van Python. Ook vereist QGIS Server dat dit bestand een functie classFactory() bevat, die wordt aangeroepen als de plug-in wordt geladen in QGIS Server. Het ontvangt een verwijzing naar de instantie van QgsServerInterface en moet een instantie teruggeven van de klasse van uw plug-in. Zo zou de voorbeeldplug-in __init__.py er uit zien.

# -*- coding: utf-8 -*-

def serverClassFactory(serverIface):
    from HelloServer import HelloServerServer
    return HelloServerServer(serverIface)

HelloServer.py

Dit is waar de magie gebeurt en dit is hoe de magie eruit ziet: (bijv. HelloServer.py)

Een plug-in voor de server bestaat gewoonlijk uit één of meer callbacks, verpakt in objecten, genaamd QgsServerFilter.

Elk QgsServerFilter implementeert één of meer van de volgende callbacks:

  • requestReady()
  • responseComplete()
  • sendResponse()

Het volgende voorbeeld implementeert een minimaal filter dat HelloServer! afdrukt in het geval dat de parameter SERVICE gelijk is aan “HELLO”:

from qgis.server import *
from qgis.core import *

class HelloFilter(QgsServerFilter):

    def __init__(self, serverIface):
        super(HelloFilter, self).__init__(serverIface)

    def responseComplete(self):
        request = self.serverInterface().requestHandler()
        params = request.parameterMap()
        if params.get('SERVICE', '').upper() == 'HELLO':
            request.clearHeaders()
            request.setHeader('Content-type', 'text/plain')
            request.clearBody()
            request.appendBody('HelloServer!')

De filters moeten worden geregistreerd in de serverIface zoals in het volgende voorbeeld:

class HelloServerServer:
    def __init__(self, serverIface):
        # Save reference to the QGIS server interface
        self.serverIface = serverIface
        serverIface.registerFilter( HelloFilter, 100 )

De tweede parameter van registerFilter() maakt het mogelijk een prioriteit in te stellen die de volgorde definieert voor de callbacks met dezelfde naam (de laagste prioriteit wordt het eerst uitgevoerd).

Door de drie callbacks te gebruiken, kunnen plug-ins de invoer en/of de uitvoer van de server op veel verschillende manieren manipuleren. Op elk moment heeft de instantie van de plug-in toegang tot de QgsRequestHandler via de QgsServerInterface, de QgsRequestHandler heeft veel methoden die kunnen worden gebruikt om de parameters voor de invoer te wijzigen vóór de bronverwerking door de server (door requestReady() te gebruiken) of nadat het verzoek is verwerkt door de bronservices (door sendResponse() te gebruiken).

De volgende voorbeelden behandelen enkele veel voorkomende gevallen van gebruik:

De invoer aanpassen

De voorbeeld plug-in bevat een testvoorbeeld dat parameters voor invoer wijzigt die afkomstig zijn uit de tekenreeks van de query, in dit voorbeeld wordt een nieuwe parameter ingevoerd in de (reeds geparste) parameterMap, deze parameter is dan zichtbaar voor bronservices (WMS etc.), aan het eind evan de verwerking door bronservices controleren we dat de parameter er nog steeds is:

from qgis.server import *
from qgis.core import *

class ParamsFilter(QgsServerFilter):

    def __init__(self, serverIface):
        super(ParamsFilter, self).__init__(serverIface)

    def requestReady(self):
        request = self.serverInterface().requestHandler()
        params = request.parameterMap( )
        request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')

    def responseComplete(self):
        request = self.serverInterface().requestHandler()
        params = request.parameterMap( )
        if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
            QgsMessageLog.logMessage("SUCCESS - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.INFO)
        else:
            QgsMessageLog.logMessage("FAIL    - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.CRITICAL)

Dit is een extract van wat u ziet in het logbestand:

src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
src/mapserver/qgsgetrequesthandler.cpp: 35: (parseInput) [0ms] query string is: SERVICE=HELLO&request=GetOutput
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [0ms] inserting pair REQUEST // GetOutput into the parameter map
src/mapserver/qgsserverfilter.cpp: 42: (requestReady) [0ms] QgsServerFilter plugin default requestReady called
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.requestReady
src/mapserver/qgis_map_serv.cpp: 235: (configPath) [0ms] Using default configuration file path: /home/xxx/apps/bin/admin.sld
src/mapserver/qgshttprequesthandler.cpp: 49: (setHttpResponse) [0ms] Checking byte array is ok to set...
src/mapserver/qgshttprequesthandler.cpp: 59: (setHttpResponse) [0ms] Byte array looks good, setting response...
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] RemoteConsoleFilter.responseComplete
src/mapserver/qgshttprequesthandler.cpp: 158: (sendResponse) [0ms] Sending HTTP response
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.sendResponse

Op regel 13 geeft de tekenreeks “SUCCESS” aan dat de plug-in voor de test is geslaagd.

Dezelfde techniek kan worden gebruikt om een aangepaste service te gebruiken in plaats van een bronservice: u zou bijvoorbeeld een verzoek WFS SERVICE kunnen overslaan of elk ander bronverzoek door slechts de parameter SERVICE naar iets anders te wijzigen en de bronservice zal worden overgeslagen, dan kunt u uw aangepaste resultaten invoeren in de uitvoer en die naar de cliënt verzenden (dat is hieronder uitgelegd).

De uitvoer aanpassen of vervangen

Het voorbeeld watermark filter laat zien hoe de uitvoer van WMS te vervangen door een nieuwe afbeelding die wordt verkregen door het toevoegen van een afbeelding van een watermerk bovenop de afbeelding van WMS die werd gegenereerd door de bronservice van WMS:

import os

from qgis.server import *
from qgis.core import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *


class WatermarkFilter(QgsServerFilter):

    def __init__(self, serverIface):
        super(WatermarkFilter, self).__init__(serverIface)

    def responseComplete(self):
        request = self.serverInterface().requestHandler()
        params = request.parameterMap( )
        # Do some checks
        if (request.parameter('SERVICE').upper() == 'WMS' \
                and request.parameter('REQUEST').upper() == 'GETMAP' \
                and not request.exceptionRaised() ):
            QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.infoFormat(), 'plugin', QgsMessageLog.INFO)
            # Get the image
            img = QImage()
            img.loadFromData(request.body())
            # Adds the watermark
            watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
            p = QPainter(img)
            p.drawImage(QRect( 20, 20, 40, 40), watermark)
            p.end()
            ba = QByteArray()
            buffer = QBuffer(ba)
            buffer.open(QIODevice.WriteOnly)
            img.save(buffer, "PNG")
            # Set the body
            request.clearBody()
            request.appendBody(ba)

In dit voorbeeld is de waarde van de parameter SERVICE gecontroleerd en als het inkomende verzoek een WMS GETMAP is en er geen uitzonderingen zijn ingesteld door een eerder uitgevoerde plug-in of door de bronservice (WMS in dit geval), wordt de door WMS gegenereerde afbeelding opgehaald uit de buffer voor de uitvoer en wordt de afbeelding van het watermerk toegevoegd. De laatste stap is om de buffer voor de uitvoer op te schonen en die te vervangen door de nieuw gegenereerde afbeelding. Onthoud dat in een situatie in de echte wereld we ook het type van de verzochte afbeelding zouden controleren in plaats van PNG in elk geval terug te geven.

Plug-in Access control

Plug-inbestanden

Hier is de mappenstructuur van onze voorbeeld-plug-in voor de server:

PYTHON_PLUGINS_PATH/
  MyAccessControl/
    __init__.py    --> *required*
    AccessControl.py  --> *required*
    metadata.txt   --> *required*

__init__.py

Dit bestand wordt vereist door het systeem voor importeren van Python. Net als voor alle QGIS Server plug-ins bevat dit bestand een functie serverClassFactory() die wordt aangeroepen als de plug-in wordt geladen in QGIS Server. Het ontvangt een verwijzing naar de instantie van QgsServerInterface en moet een instantie teruggeven van de klasse van uw plug-in. Zo zou de voorbeeldplug-in __init__.py er uit zien:

# -*- coding: utf-8 -*-

def serverClassFactory(serverIface):
    from MyAccessControl.AccessControl import AccessControl
    return AccessControl(serverIface)

AccessControl.py

class AccessControl(QgsAccessControlFilter):

    def __init__(self, server_iface):
        super(QgsAccessControlFilter, self).__init__(server_iface)

    def layerFilterExpression(self, layer):
        """ Return an additional expression filter """
        return super(QgsAccessControlFilter, self).layerFilterExpression(layer)

    def layerFilterSubsetString(self, layer):
        """ Return an additional subset string (typically SQL) filter """
        return super(QgsAccessControlFilter, self).layerFilterSubsetString(layer)

    def layerPermissions(self, layer):
        """ Return the layer rights """
        return super(QgsAccessControlFilter, self).layerPermissions(layer)

    def authorizedLayerAttributes(self, layer, attributes):
        """ Return the authorised layer attributes """
        return super(QgsAccessControlFilter, self).authorizedLayerAttributes(layer, attributes)

    def allowToEdit(self, layer, feature):
        """ Are we authorise to modify the following geometry """
        return super(QgsAccessControlFilter, self).allowToEdit(layer, feature)

    def cacheKey(self):
        return super(QgsAccessControlFilter, self).cacheKey()

Dit voorbeeld geeft een voorbeeld voor volledige toegang voor iedereen.

Het is de rol van de plug-in om te weten wie er is ingelogd.

Voor al deze methoden hebben de laag als argument om in staat te zien om de rechten per laag aan te passen.

layerFilterExpression

Gebruikt om een Expressie toe te voegen om de resultaten te beperken, bijv.:

def layerFilterExpression(self, layer):
    return "$role = 'user'"

Te beperken tot de mogelijkheid waar de rol attribuut gelijk is aan “user”.

layerFilterSubsetString

Hetzelfde als hiervoor maar dan door de SubsetString te gebruiken (uitgevoerd in de database)

def layerFilterSubsetString(self, layer):
    return "role = 'user'"

Te beperken tot de mogelijkheid waar de rol attribuut gelijk is aan “user”.

layerPermissions

Toegang beperken tot de laag.

Geef een object terug van het type QgsAccessControlFilter.LayerPermissions, die de eigenschappen heeft:

  • canRead om hem te zien in de GetCapabilities en rechten voor lezen hebben.

  • canInsert om een nieuw object in te kunnen voeren.

  • canUpdate om een object bij te kunnen werken.

  • candelete om een object te kunnen verwijderen.

Voorbeeld:

def layerPermissions(self, layer):
    rights = QgsAccessControlFilter.LayerPermissions()
    rights.canRead = True
    rights.canRead = rights.canInsert = rights.canUpdate = rights.canDelete = False
    return rights

Om alles te beperken tot toegang voor alleen-lezen.

authorizedLayerAttributes

Gebruikt om de zichtbaarheid van een specifieke subset van attributen te beperken.

Het argument attribute geeft de huidige set van zichtbare attributen terug.

Voorbeeld:

def authorizedLayerAttributes(self, layer, attributes):
    return [a for a in attributes if a != "role"]

Het attribuut ‘role’ verbergen.

allowToEdit

Dit wordt gebruikt om het bewerken van een subset van objecten te beperken.

Het wordt gebruikt in het protocol WFS-Transaction.

Voorbeeld:

def allowToEdit(self, layer, feature):
    return feature.attribute('role') == 'user'

Om het mogelijk te maken alleen objecten te bewerken die het attribuut role hebben met de waarde user.

cacheKey

QGIS server onderhoudt een cache van de capabilities, om dan een cache per rol te hebben kunt u de rol teruggeven met deze methode. Of geef None terug om de cache volledig uit te schakelen.