Plugin-uri Python pentru Serverul QGIS

Python plugins can also run on QGIS Server (see QGIS ca și Server de Date OGC): 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.

Arhitectura Plugin-urilor de Filtrare de pe Server

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()

Toate filtrele au acces la obiectul cerere/răspuns (QgsRequestHandler), putându-i manipula toate proprietățile (de intrare/ieșire) și tratându-i excepțiile (deși într-un mod cu totul particular, după cum vom vedea mai jos).

Mai jos se află un pseudocod care prezintă o sesiune tipică de server și reapelarea filtrelor:

  • se obține cererea de intrare
    • se creează o rutină de tratare a cererilor GET/POST/SOAP

    • se transmite cererea către o instanță a clasei QgsServerInterface

    • se apelează filtrele requestReady() ale plugin-urillor

    • în cazul în care nu există un răspuns
      • dacă SERVICE este de tipul WMS/WFS/WCS
        • se creează serverul WMS/WFS/WCS
          • call server’s executeRequest() and possibly call sendResponse() plugin filters when streaming output or store the byte stream output and content type in the request handler
      • se apelează filtrele responseComplete() ale plugin-urillor

    • se apelează filtrele sendResponse() ale plugin-urillor

    • request handler output the response

Următoarele paragrafe descriu, în detaliu, funcțiile de reapelare disponibile.

requestReady

Este apelată atunci când cererea este pregătită: adresa și datele primite au fost analizate și, înainte de a intra în comutatorul serviciilor de bază (WMS, WFS, etc), acesta este punctul în care se poate interveni asupra datelor de intrare, putându-se efectua acțiuni de genul:

  • autentificare/autorizare

  • redirectări

  • adăugarea/eliminarea anumitor parametri (denumirile tipurilor, de exemplu)

  • tratarea excepțiilor

Ați putea chiar să substituiți în întregime un serviciu de bază, prin schimbarea parametrului SERVICE, astfel, ocolindu-se complet serviciul de bază (deși, acest lucru nu ar avea prea mult sens).

sendResponse

This is called whenever output is sent to FCGI stdout (and from there, to the client), this is normally done after core services have finished their process and after responseComplete hook was called, but in a few cases XML can become so huge that a streaming XML implementation was needed (WFS GetFeature is one of them), in this case, sendResponse() is called multiple times before the response is complete (and before responseComplete() is called). The obvious consequence is that sendResponse() is normally called once but might be exceptionally called multiple times and in that case (and only in that case) it is also called before responseComplete().

sendResponse() reprezintă cel mai bun loc pentru manipularea directă a rezultatului serviciilor de bază și, în timp ce responseComplete() constă, de asemenea, într-o opțiune, sendResponse() este singura opțiune viabilă în cazul serviciilor de redare continuă a fluxului.

responseComplete

This is called once when core services (if hit) finish their process and the request is ready to be sent to the client. As discussed above, this is normally called before sendResponse() except for streaming services (or other plugin filters) that might have called sendResponse() earlier.

responseComplete() reprezintă locul ideal pentru implementarea unor noi servicii (WPS sau servicii personalizate), precum și pentru a efectua intervenții directe asupra rezultatului care provine de la serviciile de bază (de exemplu, pentru a adăuga un filigran pe o imagine WMS).

Tratarea excepțiilor provenite de la un plugin

Mai sunt ceva acțiuni de efectuat în acest capitol: implementarea curentă poate distinge între excepțiile tratate și cele netratate, prin setarea proprietății QgsRequestHandler pentru o instanță a clasei QgsMapServiceException; în acest fel, codul C++ principal poate să intercepteze excepțiile Python tratate și să le ignore pe cele netratate (sau mai bine: să le jurnalizeze).

Această abordare funcționează în principiu, dar nu este în spiritul limbajului “python”: o abordare mai bună ar fi de a face vizibile excepțiile din codul python la nivelul buclei C++, pentru a fi manipulată acolo.

Scrierea unui plugin pentru server

A server plugins is just a standard QGIS Python plugin as described in Dezvoltarea plugin-urilor Python, 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.

Pentru a spune Serverului QGIS că un plugin are o interfață de server, este necesară o intrare de metadate specială (în metadata.txt)

server=True

The example plugin discussed here (with many more example filters) is available on github: QGIS HelloServer Example Plugin

Fișierele Plugin-ului

Iată structura de directoare a exemplului nostru de plugin pentru 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

Aici este locul în care se întâmplă magia, și iată rezultatul acesteia: (de exemplu HelloServer.py)

Un plug-in de server este format, de obicei, dintr-una sau mai multe funcții Callback, ambalate în obiecte denumite QgsServerFilter.

Fiecare QgsServerFilter implementează una sau mai multe dintre următoarele funcții callback:

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

Exemplul următor implementează un filtru minimal, care generează textul HelloServer! atunci când parametrul SERVICE este egal cu “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!')

Filtrele trebuie să fie înregistrate în serverIface ca în exemplul următor:

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

Al doilea parametru al funcției registerFilter() permite stabilirea unei priorități, care definește ordinea pentru funcțiile callback cu același nume (prioritatea inferioară este invocată mai întâi).

Prin utilizarea celor trei funcții callback, plugin-urile pot manipula intrarea și/sau ieșirea serverului în mai multe moduri diferite. În orice moment, instanța plugin-ului are acces la QgsRequestHandler prin intermediul clasei QgsServerInterface. Clasa QgsRequestHandler dispune de o mulțime de metode care pot fi utilizate pentru modificarea parametrilor de intrare, înainte de a intra în nucleul de prelucrare al serverului (prin utilizarea requestReady()) sau în urma procesării cererii de către serviciile de bază (prin utilizarea sendResponse()).

Următorul exemplu demonstrează câteva cazuri de utilizare obișnuită:

Modificarea intrării

The example plugin contains a test example that changes input parameters coming from the query string, in this example a new parameter is injected into the (already parsed) parameterMap, this parameter is then visible by core services (WMS etc.), at the end of core services processing we check that the parameter is still there:

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)

This is an extract of what you see in the log file:

 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

On the highlighted line the “SUCCESS” string indicates that the plugin passed the test.

Aceeași tehnică poate fi exploatată pentru a utiliza un serviciu personalizat in locul unuia de bază: de exemplu, ați putea sări peste o cerere WFS SERVICE, sau peste oricare altă cerere de bază, doar prin schimbarea parametrului SERVICE în ceva diferit, iar serviciul de bază va fi omis; în acel caz, veți puteți injecta datele dvs. în interiorul rezultatului, trimițându-le clientului (acest lucru este explicat în continuare).

Modificarea sau înlocuirea rezultatului

The watermark filter example shows how to replace the WMS output with a new image obtained by adding a watermark image on the top of the WMS image generated by the WMS core service:

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)

În cadrul acestui exemplu, este verificată valoarea parametrului SERVICE, iar în cazul în care cererea de intrare este un WMS GETMAP și nici un fel de excepții nu au fost stabilite de către un plugin executat anterior, sau de către serviciul de bază (WMS în acest caz), imaginea generată de către WMS este preluată din zona tampon de ieșire, adăugându-i-se imaginea filigran. Pasul final este de a goli tamponul de ieșire și de-l înlocui cu imaginea nou generată. Rețineți că într-o situație reală, ar trebui, de asemenea, să verificați tipul imaginii solicitate în loc de a returna, în toate cazurile, PNG-ul.

Plugin-ul de control al accesului

Fișierele Plugin-ului

Iată structura de directoare a exemplului nostru de plugin pentru 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()

Acest exemplu oferă un acces deplin pentru oricine.

Este de datoria plugin-ului să știe cine este conectat.

Toate aceste metode au ca argument stratul, pentru a putea personaliza restricțiile pentru fiecare strat.

layerFilterExpression

Se folosește pentru a adăuga o Expresie de limitare a rezultatelor, ex.:

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

Pentru restrângerea la entitățile pentru care atributul “rol” are valoarea “user”.

layerFilterSubsetString

La fel ca și precedenta, dar folosește SubsetString (executată în baza de date)

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

Pentru restrângerea la entitățile pentru care atributul “rol” are valoarea “user”.

layerPermissions

Limitează accesul la strat.

Returnează un obiect de tip QgsAccessControlFilter.LayerPermissions, care are proprietățile:

  • canRead pentru a-l vedea în GetCapabilities și pentru a avea acces de citire.

  • canInsert pentru a putea insera o nouă entitate.

  • canUpdate pentru a putea actualiza o entitate.

  • candelete pentru a fi putea șterge o entitate.

Exemplu:

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

Pentru a permite tuturor accesul numai pentru citire.

authorizedLayerAttributes

Folosit pentru a reduce vizibilitatea unui subset specific de atribute.

Atributul argument returnează setul actual de atribute vizibile.

Exemplu:

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

Pentru a ascunde atributul ‘role’.

allowToEdit

Se folosește pentru a limita editarea unui subset specific de entități.

Este folosit în protocolul WFS-Transaction.

Exemplu:

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

Pentru a putea modifica numai entitatea pentru care atributul “rol” are valoarea de “utilizator”.

cacheKey

Serverul QGIS menține o memorie tampon a capabilităților, de aceea, pentru a avea o memorie cache pentru fiecare rol, puteți specifica rolul cu ajutorul acestei metode. Sau puteți seta valoarea None, pentru a dezactiva complet memoria tampon.