Outdated version of the documentation. Find the latest one here.

Python plug-ins voor QGIS server

Python plugins can also run on QGIS Server (see QGIS als OGC Data Server): by using the server interface (QgsServerInterface) a Python plugin running on the server can alter the behavior of existing core services (WMS, WFS etc.).

With the server filter interface (QgsServerFilter) we can change the input parameters, change the generated output or even by providing new services.

With the access control interface (QgsAccessControlFilter) we can apply some access restriction per requests.

Server Filter Plugins architecture

Server python plugins are loaded once when the FCGI application starts. They register one or more QgsServerFilter (from this point, you might find useful a quick look to the server plugins API docs). Each filter should implement at least one of three callbacks:

  • 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 de servers 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 normaal 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

A server plugins is just a standard QGIS Python plugin as described in Python plug-ins ontwikkelen, that just provides an additional (or alternative) interface: a typical QGIS desktop plugin has access to QGIS application through the QgisInterface instance, a server plugin has also access to a 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

The example plugin discussed here (with many more example filters) is available on 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

This file is required by Python’s import system. Also, QGIS Server requires that this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of QgsServerInterface and must return instance of your plugin’s class. This is how the example plugin __init__.py looks like:

# -*- 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 einde van de verwerking door bronservices controleren we of 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 de geaccentueerde regel 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

This file is required by Python’s import system. As for all QGIS server plugins, this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of QgsServerInterface and must return instance of your plugin’s class. This is how the example plugin __init__.py looks like:

# -*- 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.