[server] Server api and WFS3 (#10016)

Implementation of OGC API and WFS3 core draft specification
This commit is contained in:
Alessandro Pasotti 2019-08-06 16:38:21 +02:00 committed by GitHub
parent 8d44f84cd0
commit 92ac7a2e93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 13089 additions and 61 deletions

3345
external/inja/inja.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -255,6 +255,7 @@ IF (WITH_SERVER AND WITH_SERVER_PLUGINS)
INCLUDE_DIRECTORIES(
../src/server
${CMAKE_BINARY_DIR}/src/server
${CMAKE_SOURCE_DIR}/external
)
SET(PY_MODULES ${PY_MODULES} server)

View File

@ -250,6 +250,7 @@ Returns a GeoJSON string representation of a list of features (feature collectio
.. seealso:: :py:func:`exportFeature`
%End
};

View File

@ -749,6 +749,21 @@ Retrieve a list of matching registered layers by layer name.
.. seealso:: :py:func:`mapLayers`
%End
QList<QgsMapLayer *> mapLayersByShortName( const QString &shortName ) const;
%Docstring
Retrieves a list of matching registered layers by layer ``shortName``.
If layer's short name is empty a match with layer's name is attempted.
:return: list of matching layers
.. seealso:: :py:func:`mapLayer`
.. seealso:: :py:func:`mapLayers`
.. versionadded:: 3.10
%End
QMap<QString, QgsMapLayer *> mapLayers( const bool validOnly = false ) const;
%Docstring
Returns a map of all registered layers by layer ID.

View File

@ -103,6 +103,8 @@ done:
%Include conversions.sip
%Include qgsexception.sip
%Include typedefs.sip
%Include std.sip
%Include core_auto.sip

42
python/core/std.sip Normal file
View File

@ -0,0 +1,42 @@
/* std:: conversions */
%MappedType std::string
{
%TypeHeaderCode
#include <string>
%End
%ConvertFromTypeCode
// convert an std::string to a Python (unicode) string
PyObject* newstring;
newstring = PyUnicode_DecodeUTF8(sipCpp->c_str(), sipCpp->length(), NULL);
if(newstring == NULL) {
PyErr_Clear();
newstring = PyUnicode_FromString(sipCpp->c_str());
}
return newstring;
%End
%ConvertToTypeCode
// Allow a Python string (or a unicode string) whenever a string is
// expected.
// If argument is a Unicode string, just decode it to UTF-8
if (sipIsErr == NULL)
return (PyUnicode_Check(sipPy));
if (sipPy == Py_None) {
*sipCppPtr = new std::string;
return 1;
}
if (PyUnicode_Check(sipPy)) {
Py_ssize_t size;
char *s = PyUnicode_AsUTF8AndSize(sipPy, &size);
if (!s) {
return NULL;
}
*sipCppPtr = new std::string(s);
return 1;
}
return 0;
%End
};

View File

@ -0,0 +1,3 @@
# The following has been generated automatically from src/server/qgsserverogcapi.h
QgsServerOgcApi.Rel.baseClass = QgsServerOgcApi
QgsServerOgcApi.ContentType.baseClass = QgsServerOgcApi

View File

@ -0,0 +1,10 @@
# The following has been generated automatically from src/server/qgsserverquerystringparameter.h
# monkey patching scoped based enum
QgsServerQueryStringParameter.Type.String.__doc__ = ""
QgsServerQueryStringParameter.Type.Integer.__doc__ = ""
QgsServerQueryStringParameter.Type.Double.__doc__ = ""
QgsServerQueryStringParameter.Type.Boolean.__doc__ = ""
QgsServerQueryStringParameter.Type.List.__doc__ = ""
QgsServerQueryStringParameter.Type.__doc__ = 'The Type enum represents the parameter type\n\n' + '* ``String``: ' + QgsServerQueryStringParameter.Type.String.__doc__ + '\n' + '* ``Integer``: ' + QgsServerQueryStringParameter.Type.Integer.__doc__ + '\n' + '* ``Double``: ' + QgsServerQueryStringParameter.Type.Double.__doc__ + '\n' + '* ``Boolean``: ' + QgsServerQueryStringParameter.Type.Boolean.__doc__ + '\n' + '* ``List``: ' + QgsServerQueryStringParameter.Type.List.__doc__
# --
QgsServerQueryStringParameter.Type.baseClass = QgsServerQueryStringParameter

View File

@ -61,7 +61,10 @@ Returns a pointer to the server interface
void initPython();
%Docstring
Initialize Python
Note: not in Python bindings
.. note::
not available in Python bindings
%End
private:

View File

@ -0,0 +1,130 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapi.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerApi
{
%Docstring
Server generic API endpoint abstract base class.
.. seealso:: :py:class:`QgsServerOgcApi`
An API must have a name and a (possibly empty) version and define a
(possibly empty) root path (e.g. "/wfs3").
The server routing logic will check incoming request URLs by passing them
to the API's accept(url) method, the default implementation performs a simple
check for the presence of the API's root path string in the URL.
This simple logic implies that APIs must be registered in reverse order from the
most specific to the most generic: given two APIs with root paths '/wfs' and '/wfs3',
'/wfs3' must be registered first or it will be shadowed by '/wfs'.
APIs developers are encouraged to implement a more robust accept(url) logic by
making sure that their APIs accept only URLs they can actually handle, if they do,
the APIs registration order becomes irrelevant.
After the API has been registered to the server API registry:
.. code-block:: python
class API(QgsServerApi):
def name(self):
return "Test API"
def rootPath(self):
return "/testapi"
def executeRequest(self, request_context):
request_context.response().write(b"\"Test API\"")
server = QgsServer()
api = API(server.serverInterface())
server.serverInterface().serviceRegistry().registerApi(api)
the incoming calls with an URL path starting with the API root path
will be routed to the first matching API and executeRequest() method
of the API will be invoked.
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverapi.h"
%End
public:
QgsServerApi( QgsServerInterface *serverIface );
%Docstring
Creates a QgsServerApi object
%End
virtual ~QgsServerApi();
virtual const QString name() const = 0;
%Docstring
Returns the API name
%End
virtual const QString description() const = 0;
%Docstring
Returns the API description
%End
virtual const QString version() const;
%Docstring
Returns the version of the service
.. note::
the default implementation returns an empty string
%End
virtual const QString rootPath() const = 0;
%Docstring
Returns the root path for the API
%End
virtual bool allowMethod( QgsServerRequest::Method ) const;
%Docstring
Returns ``True`` if the given method is supported by the API, default implementation supports all methods.
%End
virtual bool accept( const QUrl &url ) const;
%Docstring
Returns ``True`` if the given ``url`` is handled by the API, default implementation checks for the presence of rootPath inside the ``url`` path.
%End
virtual void executeRequest( const QgsServerApiContext &context ) const = 0;
%Docstring
Executes a request by passing the given ``context`` to the API handlers.
%End
QgsServerInterface *serverIface() const;
%Docstring
Returns the server interface
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapi.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -0,0 +1,100 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapicontext.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerApiContext
{
%Docstring
The QgsServerApiContext class encapsulates the resources for a particular client
request: the request and response objects, the project (might be NULL) and
the server interface, the API root path that matched the request is also added.
QgsServerApiContext is lightweight copyable object meant to be passed along the
request handlers chain.
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverapicontext.h"
%End
public:
QgsServerApiContext( const QString &apiRootPath, const QgsServerRequest *request, QgsServerResponse *response,
const QgsProject *project, QgsServerInterface *serverInterface );
%Docstring
QgsServerApiContext constructor
:param apiRootPath: is the API root path, this information is used by the
handlers to build the href links to the resources and to the HTML templates.
:param request: the incoming request
:param response: the response
:param project: the project (might be NULL)
:param serverInterface: the server interface
%End
const QgsServerRequest *request() const;
%Docstring
Returns the server request object
%End
QgsServerResponse *response() const;
%Docstring
Returns the server response object
%End
const QgsProject *project() const;
%Docstring
Returns the (possibly NULL) project
.. seealso:: :py:func:`setProject`
%End
void setProject( const QgsProject *project );
%Docstring
Sets the project to ``project``
.. seealso:: :py:func:`project`
%End
QgsServerInterface *serverInterface() const;
%Docstring
Returns the server interface
%End
const QString matchedPath( ) const;
%Docstring
Returns the initial part of the incoming request URL path that matches the
API root path.
If there is no match returns an empty string (it should never happen).
I.e. for an API with root path "/wfs3" and an incoming request
"https://www.qgis.org/services/wfs3/collections"
this method will return "/resources/wfs3"
%End
QString apiRootPath() const;
%Docstring
Returns the API root path
%End
void setRequest( const QgsServerRequest *request );
%Docstring
Sets context request to ``request``
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapicontext.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -0,0 +1,94 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapiutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerApiUtils
{
%Docstring
The QgsServerApiUtils class contains helper functions to handle common API operations.
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverapiutils.h"
%End
public:
static QgsRectangle parseBbox( const QString &bbox );
%Docstring
Parses a comma separated ``bbox`` into a (possibily empty) :py:class:`QgsRectangle`.
.. note::
Z values (i.e. a 6 elements bbox) are silently discarded
%End
static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs );
%Docstring
Parses the CRS URI ``bboxCrs`` (example: "http://www.opengis.net/def/crs/OGC/1.3/CRS84") into a QGIS CRS object
%End
static const QgsFields publishedFields( const QgsVectorLayer *layer );
%Docstring
Returns the list of fields accessible to the service for a given ``layer``.
This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
TODO: implement ACL
%End
static const QVector<QgsMapLayer *> publishedWfsLayers( const QgsProject *project );
%Docstring
Returns the list of layers accessible to the service for a given ``project``.
This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
.. note::
project must not be NULL
TODO: implement ACL
%End
static QString sanitizedFieldValue( const QString &value );
%Docstring
Sanitizes the input ``value`` by removing URL encoding and checking for malicious content.
In case of failure returns an empty string.
%End
static QStringList publishedCrsList( const QgsProject *project );
%Docstring
Returns the list of CRSs (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84) available for this ``project``.
Information is read from project WMS configuration.
%End
static QString crsToOgcUri( const QgsCoordinateReferenceSystem &crs );
%Docstring
Returns a ``crs`` as OGC URI (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84)
Returns an empty string on failure.
%End
static QString appendMapParameter( const QString &path, const QUrl &requestUrl );
%Docstring
Appends MAP query string parameter from current ``requestUrl`` to the given ``path``
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverapiutils.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -11,6 +11,8 @@
class QgsServerException
{
%Docstring
@ -94,6 +96,7 @@ Returns the exception version
};
/************************************************************************
* This file has been generated automatically from *
* *

View File

@ -0,0 +1,140 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverogcapi.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerOgcApi : QgsServerApi
{
%Docstring
QGIS Server OGC API endpoint. QgsServerOgcApi provides the foundation for
the new generation of REST-API based OGC services (e.g. WFS3).
This class can be used directly and configured by registering handlers
as instances of QgsServerOgcApiHandler.
.. code-block:: python
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverogcapi.h"
%End
public:
static const QMetaObject staticMetaObject;
public:
enum Rel
{
// The following registered link relation types are used
alternate,
describedBy,
collection,
item,
self,
service_desc,
service_doc,
prev,
next,
license,
// In addition the following link relation types are used for which no applicable registered link relation type could be identified:
items,
conformance,
data
};
enum ContentType
{
GEOJSON,
OPENAPI3,
JSON,
HTML
};
QgsServerOgcApi( QgsServerInterface *serverIface,
const QString &rootPath,
const QString &name,
const QString &description = QString(),
const QString &version = QString() );
%Docstring
QgsServerOgcApi constructor
:param serverIface: pointer to the server interface
:param rootPath: root path for this API (usually starts with a "/", e.g. "/wfs3")
:param name: API name
:param description: API description
:param version: API version
%End
virtual const QString name() const;
virtual const QString description() const;
virtual const QString version() const;
virtual const QString rootPath() const;
~QgsServerOgcApi();
virtual void executeRequest( const QgsServerApiContext &context ) const throw( QgsServerApiBadRequestException ) /VirtualErrorHandler=serverapi_badrequest_exception_handler/;
%Docstring
Executes a request by passing the given ``context`` to the API handlers.
%End
void registerHandler( QgsServerOgcApiHandler *handler /Transfer/ );
%Docstring
Registers an OGC API ``handler``, ownership of the handler is transferred to the API
%End
static QUrl sanitizeUrl( const QUrl &url );
%Docstring
Returns a sanitized ``url`` with extra slashes removed
%End
static std::string relToString( const QgsServerOgcApi::Rel &rel );
%Docstring
Returns the string representation of ``rel`` attribute.
%End
static QString contentTypeToString( const QgsServerOgcApi::ContentType &ct );
%Docstring
Returns the string representation of a ``ct`` (Content-Type) attribute.
%End
static std::string contentTypeToStdString( const QgsServerOgcApi::ContentType &ct );
%Docstring
Returns the string representation of a ``ct`` (Content-Type) attribute.
%End
static QString contentTypeToExtension( const QgsServerOgcApi::ContentType &ct );
%Docstring
Returns the file extension for a ``ct`` (Content-Type).
%End
static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension );
%Docstring
Returns the Content-Type value corresponding to ``extension``.
%End
static std::string mimeType( const QgsServerOgcApi::ContentType &contentType );
%Docstring
Returns the mime-type for the ``contentType`` or an empty string if not found
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverogcapi.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -0,0 +1,229 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverogcapihandler.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerOgcApiHandler
{
%Docstring
The QgsServerOgcApiHandler abstract class represents a OGC API handler to be registered
in QgsServerOgcApi class.
Subclasses must override operational and informative methods and define
the core functionality in handleRequest() method.
The following methods MUST be implemented:
- path
- operationId
- summary (shorter text)
- description (longer text)
- linkTitle
- linkType
- schema
Optionally, override:
- tags
- parameters
- contentTypes
- defaultContentType
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverogcapihandler.h"
%End
public:
virtual ~QgsServerOgcApiHandler();
virtual QRegularExpression path() const = 0;
%Docstring
URL pattern for this handler, named capture group are automatically
extracted and returned by values()
Example: "/handlername/(?P<code1>\d{2})/items" will capture "code1" as a
named parameter.
.. seealso:: :py:func:`values`
%End
virtual std::string operationId() const = 0;
%Docstring
Returns the operation id for template file names and other internal references
%End
virtual QList<QgsServerQueryStringParameter> parameters( const QgsServerApiContext &context ) const;
%Docstring
Returns a list of query string parameters.
Depending on the handler, it may be dynamic (per-request) or static.
:param context: the request context
%End
virtual std::string summary() const = 0;
%Docstring
Summary
%End
virtual std::string description() const = 0;
%Docstring
Description
%End
virtual std::string linkTitle() const = 0;
%Docstring
Title for the handler link
%End
virtual QgsServerOgcApi::Rel linkType() const = 0;
%Docstring
Main role for the resource link
%End
virtual QStringList tags() const;
%Docstring
Tags
%End
virtual QgsServerOgcApi::ContentType defaultContentType() const;
%Docstring
Returns the default response content type in case the client did not specifically
ask for any particular content type.
%End
virtual QList<QgsServerOgcApi::ContentType> contentTypes() const;
%Docstring
Returns the list of content types this handler can serve, default to JSON and HTML.
In case a specialized type (such as GEOJSON) is supported,
the generic type (such as JSON) should not be listed.
%End
virtual void handleRequest( const QgsServerApiContext &context ) const = 0;
%Docstring
Handles the request within its ``context``
Subclasses must implement this methods, and call validate() to
extract validated parameters from the request.
\throws QgsServerApiBadRequestError if the method encounters any error
%End
virtual QVariantMap values( const QgsServerApiContext &context ) const throw( QgsServerApiBadRequestException );
%Docstring
Analyzes the incoming request ``context`` and returns the validated
parameter map, throws QgsServerApiBadRequestError in case of errors.
Path fragments from the named groups in the path() regular expression
are also added to the map.
Your handleRequest method should call this function to retrieve
the parameters map.
:return: the validated parameters map by extracting captured
named parameters from the path (no validation is performed on
the type because the regular expression can do it),
and the query string parameters.
.. seealso:: :py:func:`path`
.. seealso:: :py:func:`parameters`
\throws QgsServerApiBadRequestError if validation fails
%End
QString contentTypeForAccept( const QString &accept ) const;
%Docstring
Looks for the first ContentType match in the accept header and returns its mime type,
returns an empty string if there are not matches.
%End
void write( QVariant &data, const QgsServerApiContext &context, const QVariantMap &htmlMetadata = QVariantMap() ) const;
%Docstring
Writes ``data`` to the ``context`` response stream, content-type is calculated from the ``context`` request,
optional ``htmlMetadata`` for the HTML templates can be specified and will be added as "metadata" to
the HTML template variables.
HTML output uses a template engine.
Available template functions:
See: https://github.com/pantor/inja#tutorial
Available custom template functions:
- path_append( path ): appends a directory path to the current url
- path_chomp( n ): removes the specified number "n" of directory components from the current url path
- json_dump(): prints current JSON data passed to the template
- static( path): returns the full URL to the specified static path, for example:
static("/style/black.css") will return something like "/wfs3/static/style/black.css".
- links_filter( links, key, value ): returns filtered links from a link list
- content_type_name( content_type ): returns a short name from a content type for example "text/html" will return "HTML"
%End
std::string href( const QgsServerApiContext &context, const QString &extraPath = QString(), const QString &extension = QString() ) const;
%Docstring
Returns an URL to self, to be used for links to the current resources and as a base for constructing links to sub-resources
:param context: the current request context
:param extraPath: an optional extra path that will be appended to the calculated URL
:param extension: optional file extension to add (the dot will be added automatically).
%End
const QString templatePath( const QgsServerApiContext &context ) const;
%Docstring
Returns the HTML template path for the handler in the given ``context``
The template path is calculated from QgsServerSettings's apiResourcesDirectory() as follow:
apiResourcesDirectory() + "/ogc/templates/" + context.apiRootPath + operationId + ".html"
e.g. for an API with root path "/wfs3" and an handler with operationId "collectionItems", the path
will be apiResourcesDirectory() + "/ogc/templates/wfs3/collectionItems.html"
%End
const QString staticPath( const QgsServerApiContext &context ) const;
%Docstring
Returns the absolute path to the base directory where static resources for
this handler are stored in the given ``context``.
%End
QgsServerOgcApi::ContentType contentTypeFromRequest( const QgsServerRequest *request ) const;
%Docstring
Returns the content type from the ``request``.
The path file extension is examined first and checked for known mime types,
the "Accept" HTTP header is examined next.
Fallback to the default content type of the handler if none of the above matches.
\throws QgsServerApiBadRequestError if the content type of the request is not compatible with the handler (:py:func:`contentTypes` member)
%End
static QString parentLink( const QUrl &url, int levels = 1 );
%Docstring
Returns a link to the parent page up to ``levels`` in the HTML hierarchy from the given ``url``, MAP query argument is preserved
%End
static QgsVectorLayer *layerFromCollection( const QgsServerApiContext &context, const QString &collectionId );
%Docstring
Returns a vector layer from the ``collectionId`` in the given ``context``
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverogcapihandler.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -0,0 +1,109 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverquerystringparameter.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsServerQueryStringParameter
{
%Docstring
The QgsServerQueryStringParameter class holds the information regarding
a query string input parameter and its validation.
The class is extendable through custom validators (C++ only) and/or by
subclassing and overriding the value() method.
.. versionadded:: 3.10
%End
%TypeHeaderCode
#include "qgsserverquerystringparameter.h"
%End
public:
static const QMetaObject staticMetaObject;
public:
enum class Type
{
String,
Integer,
Double,
Boolean,
List,
};
QgsServerQueryStringParameter( const QString name,
bool required = false,
Type type = QgsServerQueryStringParameter::Type::String,
const QString &description = QString(),
const QVariant &defaultValue = QVariant() );
%Docstring
Constructs a QgsServerQueryStringParameter object.
:param name: parameter name
:param required:
:param type: the parameter type
:param description: parameter description
:param defaultValue: default value, it is ignored if the parameter is required
%End
virtual ~QgsServerQueryStringParameter();
virtual QVariant value( const QgsServerApiContext &context ) const;
%Docstring
Extracts the value from the request ``context`` by validating the parameter
value and converting it to its proper Type.
If the value is not set and a default was not provided an invalid QVariant is returned.
Validation steps:
- required
- can convert to proper Type
- custom validator (if set - not available in Python bindings)
.. seealso:: :py:func:`setCustomValidator`
:return: the parameter value or an invalid QVariant if not found (and not required)
\throws QgsServerApiBadRequestError if validation fails
%End
QString description() const;
%Docstring
Returns parameter description
%End
static QString typeName( const Type type );
%Docstring
Returns the name of the ``type``
%End
QString name() const;
%Docstring
Returns the name of the parameter
%End
void setDescription( const QString &description );
%Docstring
Sets validator ``description``
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/server/qgsserverquerystringparameter.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -87,7 +87,7 @@ Returns parameters
Set a parameter
%End
QString parameter( const QString &key ) const;
QString parameter( const QString &key, const QString &defaultValue = QString() ) const;
%Docstring
Gets a parameter value
%End
@ -153,6 +153,13 @@ by default this is equal to the url seen by QGIS server
void setMethod( QgsServerRequest::Method method );
%Docstring
Set the request method
%End
const QString queryParameter( const QString &name, const QString &defaultValue = QString( ) ) const;
%Docstring
Returns the query string parameter with the given ``name`` from the request URL, a ``defaultValue`` can be specified.
.. versionadded:: 3.10
%End
protected:

View File

@ -98,6 +98,7 @@ to the underlying I/O device
virtual void write( const QgsServerException &ex );
%Docstring
Write server exception

View File

@ -153,6 +153,26 @@ Returns the server-wide max width of a WMS GetMap request. The lower one of this
:return: the max width of a WMS GetMap request.
.. versionadded:: 3.8
%End
QString apiResourcesDirectory() const;
%Docstring
Returns the server-wide base directory where HTML templates and static assets (e.g. images, js and css files) are searched for.
The default path is calculated by joining QgsApplication.pkgDataPath() with "resources/server/api", this path
can be changed by setting the environment variable QGIS_SERVER_API_RESOURCES_DIRECTORY.
.. versionadded:: 3.10
%End
qlonglong apiWfs3MaxLimit() const;
%Docstring
Returns the server-wide maximum allowed value for \"limit\" in a features request.
The default value is 10000, this value can be changed by setting the environment
variable QGIS_SERVER_API_WFS3_MAX_LIMIT.
.. versionadded:: 3.10
%End
};

View File

@ -57,16 +57,55 @@ Register a service by its name and version
This method is intended to be called by modules for registering
services. A module may register multiple services.
The registry gain ownership of services and will call 'delete' on cleanup
The registry takes ownership of services and will call 'delete' on cleanup
:param service: a QgsService to be registered
%End
bool registerApi( QgsServerApi *api /Transfer/ );
%Docstring
Registers the :py:class:`QgsServerApi` ``api``
The registry takes ownership of services and will call 'delete' on cleanup
.. versionadded:: 3.10
%End
int unregisterApi( const QString &name, const QString &version = QString() );
%Docstring
Unregisters API from its name and version
:param name: the name of the service
:param version: (optional) the specific version to unload
:return: the number of APIs unregistered
If the version is not specified then all versions from the specified API
are unloaded
.. versionadded:: 3.10
%End
QgsServerApi *getApi( const QString &name, const QString &version = QString() );
%Docstring
Retrieves an API from its name
If the version is not provided the higher version of the service is returned
:param name: the name of the API
:param version: the version string (optional)
:return: :py:class:`QgsServerApi`
.. versionadded:: 3.10
%End
int unregisterService( const QString &name, const QString &version = QString() );
%Docstring
Unregister service from its name and version
:param name: the tame of the service
:param name: the name of the service
:param version: (optional) the specific version to unload
:return: the number of services unregistered

View File

@ -0,0 +1,23 @@
%Exception QgsServerApiBadRequestException(SIP_Exception) /PyName=QgsServerApiBadRequestException/
{
%TypeHeaderCode
#include <qgsserverexception.h>
%End
%RaiseCode
SIP_BLOCK_THREADS
PyErr_SetString(sipException_QgsServerApiBadRequestException, sipExceptionRef.what().toUtf8().constData() );
SIP_UNBLOCK_THREADS
%End
};
%Exception QgsServerApiInternalServerError(SIP_Exception) /PyName=QgsServerApiInternalServerError/
{
%TypeHeaderCode
#include <qgsserverexception.h>
%End
%RaiseCode
SIP_BLOCK_THREADS
PyErr_SetString(sipException_QgsServerApiInternalServerError, sipExceptionRef.what().toUtf8().constData() );
SIP_UNBLOCK_THREADS
%End
};

View File

@ -8,7 +8,22 @@ ${DEFAULTDOCSTRINGSIGNATURE}
%Import QtXml/QtXmlmod.sip
%Import core/core.sip
%Include qgsserverexception.sip
%Feature HAVE_SERVER_PYTHON_PLUGINS
%Include server_auto.sip
%VirtualErrorHandler serverapi_badrequest_exception_handler
PyObject *exception, *value, *traceback;
PyErr_Fetch(&exception, &value, &traceback);
SIP_RELEASE_GIL( sipGILState );
QString strVal = "API bad request error";
if ( value && PyUnicode_Check(value) )
{
Py_ssize_t size;
strVal = QString::fromUtf8( PyUnicode_AsUTF8AndSize(value, &size) );
}
throw QgsServerApiBadRequestException( strVal );
%End

View File

@ -1,6 +1,10 @@
// Include auto-generated SIP files
%Include auto_generated/qgsservicemodule.sip
%Include auto_generated/qgsmapserviceexception.sip
%Include auto_generated/qgsserverapi.sip
%Include auto_generated/qgsserverogcapi.sip
%Include auto_generated/qgsserverogcapihandler.sip
%Include auto_generated/qgsserverapicontext.sip
%Include auto_generated/qgsserverquerystringparameter.sip
%Include auto_generated/qgscapabilitiescache.sip
%Include auto_generated/qgsconfigcache.sip
%Include auto_generated/qgsserverlogger.sip
@ -11,6 +15,7 @@
%Include auto_generated/qgsfcgiserverrequest.sip
%Include auto_generated/qgsrequesthandler.sip
%Include auto_generated/qgsserver.sip
%Include auto_generated/qgsserverapiutils.sip
%Include auto_generated/qgsserverexception.sip
%If ( HAVE_SERVER_PYTHON_PLUGINS )
%Include auto_generated/qgsserverinterface.sip

View File

@ -0,0 +1,385 @@
{ "components" : {
"schemas" : {
"exception" : {
"required" : [ "code" ],
"type" : "object",
"properties" : {
"code" : {
"type" : "string"
},
"description" : {
"type" : "string"
}
}
},
"root" : {
"required" : [ "links" ],
"type" : "object",
"properties" : {
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/",
"rel" : "self",
"type" : "application/json",
"title" : "this document"
}, {
"href" : "http://data.example.org/api",
"rel" : "service",
"type" : "application/openapi+json;version=3.0",
"title" : "the API definition"
}, {
"href" : "http://data.example.org/conformance",
"rel" : "conformance",
"type" : "application/json",
"title" : "WFS 3.0 conformance classes implemented by this server"
}, {
"href" : "http://data.example.org/collections",
"rel" : "data",
"type" : "application/json",
"title" : "Metadata about the feature collections"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
}
}
},
"req-classes" : {
"required" : [ "conformsTo" ],
"type" : "object",
"properties" : {
"conformsTo" : {
"type" : "array",
"example" : [ "http://www.opengis.net/spec/wfs-1/3.0/req/core", "http://www.opengis.net/spec/wfs-1/3.0/req/oas30", "http://www.opengis.net/spec/wfs-1/3.0/req/html", "http://www.opengis.net/spec/wfs-1/3.0/req/geojson" ],
"items" : {
"type" : "string"
}
}
}
},
"link" : {
"required" : [ "href" ],
"type" : "object",
"properties" : {
"href" : {
"type" : "string",
"example" : "http://data.example.com/buildings/123"
},
"rel" : {
"type" : "string",
"example" : "prev"
},
"type" : {
"type" : "string",
"example" : "application/geo+json"
},
"hreflang" : {
"type" : "string",
"example" : "en"
}
}
},
"content" : {
"required" : [ "collections", "links" ],
"type" : "object",
"properties" : {
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/collections.json",
"rel" : "self",
"type" : "application/json",
"title" : "this document"
}, {
"href" : "http://data.example.org/collections.html",
"rel" : "alternate",
"type" : "text/html",
"title" : "this document as HTML"
}, {
"href" : "http://schemas.example.org/1.0/foobar.xsd",
"rel" : "describedBy",
"type" : "application/xml",
"title" : "XML schema for Acme Corporation data"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"collections" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/collectionInfo"
}
}
}
},
"collectionInfo" : {
"required" : [ "links", "name" ],
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "identifier of the collection used, for example, in URIs",
"example" : "buildings"
},
"title" : {
"type" : "string",
"description" : "human readable title of the collection",
"example" : "Buildings"
},
"description" : {
"type" : "string",
"description" : "a description of the features in the collection",
"example" : "Buildings in the city of Bonn."
},
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/collections/buildings/items",
"rel" : "item",
"type" : "application/geo+json",
"title" : "Buildings"
}, {
"href" : "http://example.com/concepts/buildings.html",
"rel" : "describedBy",
"type" : "text/html",
"title" : "Feature catalogue for buildings"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"extent" : {
"$ref" : "#/components/schemas/extent"
},
"crs" : {
"type" : "array",
"description" : "The coordinate reference systems in which geometries may be retrieved. Coordinate reference systems are identified by a URI. The first coordinate reference system is the coordinate reference system that is used by default. This is always \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", i.e. WGS84 longitude/latitude.",
"example" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84", "http://www.opengis.net/def/crs/EPSG/0/4326" ],
"items" : {
"type" : "string"
},
"default" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ]
},
"relations" : {
"type" : "object",
"description" : "Related collections that may be retrieved for this collection",
"example" : "{\"id\": \"label\"}"
}
}
},
"extent" : {
"required" : [ "spatial" ],
"type" : "object",
"properties" : {
"crs" : {
"type" : "string",
"description" : "Coordinate reference system of the coordinates in the spatial extent (property `spatial`). In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional coordinate reference systems.",
"enum" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ],
"default" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
},
"spatial" : {
"maxItems" : 6,
"minItems" : 4,
"type" : "array",
"description" : "West, north, east, south edges of the spatial extent. The minimum and maximum values apply to the coordinate reference system WGS84 longitude/latitude that is supported in the Core. If, for example, a projected coordinate reference system is used, the minimum and maximum values need to be adjusted.",
"example" : [ -180, -90, 180, 90 ],
"items" : {
"type" : "number"
}
}
}
},
"featureCollectionGeoJSON" : {
"required" : [ "features", "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "FeatureCollection" ]
},
"features" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/featureGeoJSON"
}
},
"links" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"timeStamp" : {
"type" : "string",
"format" : "dateTime"
},
"numberMatched" : {
"minimum" : 0,
"type" : "integer"
},
"numberReturned" : {
"minimum" : 0,
"type" : "integer"
}
}
},
"featureGeoJSON" : {
"required" : [ "geometry", "properties", "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "Feature" ]
},
"geometry" : {
"$ref" : "#/components/schemas/geometryGeoJSON"
},
"properties" : {
"type" : "object",
"nullable" : true
},
"id" : {
"oneOf" : [ {
"type" : "string"
}, {
"type" : "integer"
} ]
}
}
},
"geometryGeoJSON" : {
"required" : [ "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon", "GeometryCollection" ]
}
}
}
},
"parameters" : {
"limit" : {
"name" : "limit",
"in" : "query",
"description" : "The optional limit parameter limits the number of items that are presented in the response document.\\\nOnly items are counted that are on the first level of the collection in the response document. Nested objects contained within the explicitly requested items shall not be counted.\\\nMinimum = 1.\\\nMaximum = 10000.\\\nDefault = 10.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maximum" : 10000,
"minimum" : 1,
"type" : "integer",
"default" : 10
},
"example" : 10
},
"offset" : {
"name" : "offset",
"in" : "query",
"description" : "The optional offset parameter indicates the index within the result set from which the server shall begin presenting results in the response document. The first element has an index of 0.\\\nMinimum = 0.\\\nDefault = 0.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"minimum" : 0,
"type" : "integer",
"default" : 0
},
"example" : 0
},
"bbox" : {
"name" : "bbox",
"in" : "query",
"description" : "Only features that have a geometry that intersects the bounding box are selected. The bounding box is provided as four or six numbers, depending on whether the coordinate reference system includes a vertical axis (elevation or depth):\n \n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Lower left corner, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Upper right corner, coordinate axis 3 (optional)\n\nThe coordinate reference system of the values is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nFor WGS84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge).\n\nIf a feature has multiple spatial geometry properties, it is the decision of the server whether only a single spatial geometry property is used to determine the extent or all relevant geometries.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maxItems" : 6,
"minItems" : 4,
"type" : "array",
"items" : {
"type" : "number"
}
}
},
"time" : {
"name" : "time",
"in" : "query",
"description" : "Either a date-time or a period string that adheres to RFC 3339. Examples:\n\n* A date-time: \"2018-02-12T23:20:50Z\"\n* A period: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\" or \"2018-02-12T00:00:00Z/P1M6DT12H31M12S\"\n\nOnly features that have a temporal property that intersects the value of\n`time` are selected.\n\nIf a feature has multiple temporal properties, it is the decision of the\nserver whether only a single temporal property is used to determine\nthe extent or all relevant temporal properties.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string"
}
},
"resultType" : {
"name" : "resultType",
"in" : "query",
"description" : "This service will respond to a query in one of two ways (excluding an exception response). It may either generate a complete response document containing resources that satisfy the operation or it may simply generate an empty response container that indicates the count of the total number of resources that the operation would return. Which of these two responses is generated is determined by the value of the optional resultType parameter.\\\nThe allowed values for this parameter are \"results\" and \"hits\".\\\nIf the value of the resultType parameter is set to \"results\", the server will generate a complete response document containing resources that satisfy the operation.\\\nIf the value of the resultType attribute is set to \"hits\", the server will generate an empty response document containing no resource instances.\\\nDefault = \"results\".",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string",
"enum" : [ "hits", "results" ],
"default" : "results"
},
"example" : "results"
},
"featureId" : {
"name" : "featureId",
"in" : "path",
"description" : "Local identifier of a specific feature",
"required" : true,
"schema" : {
"type" : "string"
}
},
"relations" : {
"name" : "relations",
"in" : "query",
"description" : "Comma-separated list of related collections that should be shown for this feature",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
},
"crs" : {
"name" : "crs",
"in" : "query",
"description" : "The coordinate reference system of the response geometries. Default is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84).",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string",
"enum" : [ "http://www.opengis.net/def/crs/EPSG/0/25832", "http://www.opengis.net/def/crs/OGC/1.3/CRS84", "http://www.opengis.net/def/crs/EPSG/0/3034", "http://www.opengis.net/def/crs/EPSG/0/3035", "http://www.opengis.net/def/crs/EPSG/0/3043", "http://www.opengis.net/def/crs/EPSG/0/3044", "http://www.opengis.net/def/crs/EPSG/0/3045", "http://www.opengis.net/def/crs/EPSG/0/3857", "http://www.opengis.net/def/crs/EPSG/0/4258", "http://www.opengis.net/def/crs/EPSG/0/4326", "http://www.opengis.net/def/crs/EPSG/0/4647", "http://www.opengis.net/def/crs/EPSG/0/5649", "http://www.opengis.net/def/crs/EPSG/0/5650", "http://www.opengis.net/def/crs/EPSG/0/5651", "http://www.opengis.net/def/crs/EPSG/0/5652", "http://www.opengis.net/def/crs/EPSG/0/5653", "http://www.opengis.net/def/crs/EPSG/0/28992", "http://www.opengis.net/def/crs/EPSG/0/25831", "http://www.opengis.net/def/crs/EPSG/0/25833" ],
"default" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
},
"bbox-crs" : {
"name" : "bbox-crs",
"in" : "query",
"description" : "The coordinate reference system of the bbox parameter. Default is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84).",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string",
"enum" : [ "http://www.opengis.net/def/crs/EPSG/0/25832", "http://www.opengis.net/def/crs/OGC/1.3/CRS84", "http://www.opengis.net/def/crs/EPSG/0/3034", "http://www.opengis.net/def/crs/EPSG/0/3035", "http://www.opengis.net/def/crs/EPSG/0/3043", "http://www.opengis.net/def/crs/EPSG/0/3044", "http://www.opengis.net/def/crs/EPSG/0/3045", "http://www.opengis.net/def/crs/EPSG/0/3857", "http://www.opengis.net/def/crs/EPSG/0/4258", "http://www.opengis.net/def/crs/EPSG/0/4326", "http://www.opengis.net/def/crs/EPSG/0/4647", "http://www.opengis.net/def/crs/EPSG/0/5649", "http://www.opengis.net/def/crs/EPSG/0/5650", "http://www.opengis.net/def/crs/EPSG/0/5651", "http://www.opengis.net/def/crs/EPSG/0/5652", "http://www.opengis.net/def/crs/EPSG/0/5653", "http://www.opengis.net/def/crs/EPSG/0/28992", "http://www.opengis.net/def/crs/EPSG/0/25831", "http://www.opengis.net/def/crs/EPSG/0/25833" ],
"default" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
}
}
}
}

View File

@ -0,0 +1 @@
PRE.jsonFormatter-codeContainer{margin-top:0;margin-bottom:0}PRE.jsonFormatter-codeContainer .jsonFormatter-objectBrace{color:#0a0;font-weight:bold}PRE.jsonFormatter-codeContainer .jsonFormatter-arrayBrace{color:#03f;font-weight:bold}PRE.jsonFormatter-codeContainer .jsonFormatter-propertyName{color:#c00;font-weight:bold}PRE.jsonFormatter-codeContainer .jsonFormatter-string{color:#077}PRE.jsonFormatter-codeContainer .jsonFormatter-number{color:#a0a}PRE.jsonFormatter-codeContainer .jsonFormatter-boolean{color:#00f}PRE.jsonFormatter-codeContainer .jsonFormatter-function{color:#a63;font-style:italic}PRE.jsonFormatter-codeContainer .jsonFormatter-null{color:#00f}PRE.jsonFormatter-codeContainer .jsonFormatter-coma{color:#000;font-weight:bold}PRE.jsonFormatter-codeContainer .jsonFormatter-expander{display:inline-block;width:28px;height:11px;cursor:pointer}PRE.jsonFormatter-codeContainer .jsonFormatter-expanded{background:url('data:image/gif;base64,R0lGODlhHAALAMQAAP////7++/z8/Pb29fb18PHx7e/w6/Hw6e3s5unp4+Dg3t3a0djY0dnVy9fTxNbQxtLMv8zJurDC1L+9sMK4p32buAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEHABcALAAAAAAcAAsAAAVL4CWOZGmel1StbCWhsFgBdA1UMVwJQd8TuNypMigWD4qgsFQhWJ7PhXI5qhQKCERC0ZhSLxUFo+FwQCJeagUyobjd6aWqtXp979QQADs=') /*Expanded.gif*/ no-repeat}PRE.jsonFormatter-codeContainer .jsonFormatter-collapsed{background:url('data:image/gif;base64,R0lGODlhHAALAMQAAP////7++/z8/Pb29fb18PHx7e/w6/Hw6e3s5unp4+jm2ODg3t3a0dnVy9bQxtLMv8zJurDC1L+9sMK4p32buDMzMwAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEHABcALAAAAAAcAAsAAAVU4CWOZGmeV0StLBWhsEgBdA1QMUwJvMUTuNyJMihaBodFUFiiECxQKGMpqlSq14uVRCkUEJbEokHVZrdmrqLRsDgekDLzQoFIJni8nKlqrV5zgYIhADs=') /*Collapsed.gif*/ no-repeat}

View File

@ -0,0 +1,2 @@
(function($){$.fn.jsonFormatter=function(n){var _settings,u=new Date,r=new RegExp,i=function(n,t,i){for(var r="",u=0;u<n&&!i;u++)r+=_settings.tab;return t!=null&&t.length>0&&t.charAt(t.length-1)!="\n"&&(t=t+"\n"),r+t},f=function(n,t){for(var r,u,f="",i=0;i<n;i++)f+=_settings.tab;for(r=t.toString().split("\n"),u="",i=0;i<r.length;i++)u+=(i==0?"":f)+r[i]+"\n";return u},t=function(n,t,r,u,f,e){typeof n=="string"&&(n=n.split("<").join("&lt;").split(">").join("&gt;"));var o="<span class='"+e+"'>"+t+n+t+r+"<\/span>";return f&&(o=i(u,o)),o},_processObject=function(n,e,o,s,h){var c="",l=o?"<span class='jsonFormatter-coma'>,<\/span> ":"",v=typeof n,a="",y,p,k,w,b;if($.isArray(n))if(n.length==0)c+=i(e,"<span class='jsonFormatter-arrayBrace'>[ ]<\/span>"+l,h);else{for(a=_settings.collapsible?"<span class='jsonFormatter-expander jsonFormatter-expanded'><\/span><span class='jsonFormatter-collapsible'>":"",c+=i(e,"<span class='jsonFormatter-arrayBrace'>[<\/span>"+a,h),y=0;y<n.length;y++)c+=_processObject(n[y],e+1,y<n.length-1,!0,!1);a=_settings.collapsible?"<\/span>":"";c+=i(e,a+"<span class='jsonFormatter-arrayBrace'>]<\/span>"+l)}else if(v=="object")if(n==null)c+=t("null","",l,e,s,"jsonFormatter-null");else if(n.constructor==u.constructor)c+=t("new Date("+n.getTime()+") /*"+n.toLocaleString()+"*/","",l,e,s,"Date");else if(n.constructor==r.constructor)c+=t("new RegExp("+n+")","",l,e,s,"RegExp");else{p=0;for(w in n)p++;if(p==0)c+=i(e,"<span class='jsonFormatter-objectBrace'>{ }<\/span>"+l,h);else{a=_settings.collapsible?"<span class='jsonFormatter-expander jsonFormatter-expanded'><\/span><span class='jsonFormatter-collapsible'>":"";c+=i(e,"<span class='jsonFormatter-objectBrace'>{<\/span>"+a,h);k=0;for(w in n)b=_settings.quoteKeys?'"':"",c+=i(e+1,"<span class='jsonFormatter-propertyName'>"+b+w+b+"<\/span>: "+_processObject(n[w],e+1,++k<p,!1,!0));a=_settings.collapsible?"<\/span>":"";c+=i(e,a+"<span class='jsonFormatter-objectBrace'>}<\/span>"+l)}}else v=="number"?c+=t(n,"",l,e,s,"jsonFormatter-number"):v=="boolean"?c+=t(n,"",l,e,s,"jsonFormatter-boolean"):v=="function"?n.constructor==r.constructor?c+=t("new RegExp("+n+")","",l,e,s,"RegExp"):(n=f(e,n),c+=t(n,"",l,e,s,"jsonFormatter-function")):c+=v=="undefined"?t("undefined","",l,e,s,"jsonFormatter-null"):t(n.toString().split("\\").join("\\\\").split('"').join('\\"'),'"',l,e,s,"jsonFormatter-string");return c},e=function(element){var json=$(element).html(),obj,original;json.trim()==""&&(json='""');try{obj=eval("["+json+"]")}catch(exception){return}html=_processObject(obj[0],0,!1,!1,!1);original=$(element).wrapInner("<div class='jsonFormatter-original'><\/div>");_settings.hideOriginal===!0&&$(".jsonFormatter-original",original).hide();original.append("<PRE class='jsonFormatter-codeContainer'>"+html+"<\/PRE>")},o=function(){var n=$(this).next();n.length<1||($(this).hasClass("jsonFormatter-expanded")==!0?(n.hide(),$(this).removeClass("jsonFormatter-expanded").addClass("jsonFormatter-collapsed")):(n.show(),$(this).removeClass("jsonFormatter-collapsed").addClass("jsonFormatter-expanded")))};return _settings=$.extend({tab:" ",quoteKeys:!0,collapsible:!0,hideOriginal:!0},n),this.each(function(n,t){e(t);$(t).on("click",".jsonFormatter-expander",o)})}})(jQuery);
//# sourceMappingURL=jsonFormatter.min.js.map

View File

@ -0,0 +1,11 @@
a { color: green; }
#mapid.small {
width: 100%;
height: 400px;
}
.card-header span.small {
font-size: 70%;
}

View File

@ -0,0 +1,65 @@
<!-- template for the WFS3 API collection page -->
{% include "header.html" %}
<div class="row">
<div class="col-md-6">
<h1><a title="View items of: {{ title }}" href="{% for link in links %}
{% if link.type == "text/html" and link.rel == "items" %}
{{ link.href }}
{% endif %}
{% endfor %}">{{ title }}</a></h1>
<h3>Available CRSs</h3>
<ul>
{% for c in crs %}
<li>{{ c }}</li>
{% endfor %}
</ul>
<h3>Extent</h3>
<dl>
<dd>West</dd>
<dt>{{ extent.spatial.0.0 }}</dt>
<dd>South</dd>
<dt>{{ extent.spatial.0.1 }}</dt>
<dd>East</dd>
<dt>{{ extent.spatial.0.2 }}</dt>
<dd>North</dd>
<dt>{{ extent.spatial.0.3 }}</dt>
</dl>
</div>
<div class="col-md-6">
<div id="mapid" class="small"></div>
<script type="text/javascript">
jQuery( document ).ready(function( $ ) {
var map = L.map('mapid').setView([0, 0], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map)
var west = {{ extent.spatial.0.0 }};
var south = {{ extent.spatial.0.1 }};
var east = {{ extent.spatial.0.2 }};
var north = {{ extent.spatial.0.3 }};
var p1 = new L.LatLng(south, west);
var p2 = new L.LatLng(north, west);
var p3 = new L.LatLng(north, east);
var p4 = new L.LatLng(south, east);
var polygonPoints = [p1, p2, p3, p4];
var jl = new L.Polygon(polygonPoints).addTo(map);
map.setView(jl.getBounds().getCenter());
if ( jl.getBounds().getEast() != jl.getBounds().getWest() && jl.getBounds().getNorth() != jl.getBounds().getSouth() )
{
map.fitBounds(jl.getBounds());
}
});
</script>
</div>
</div>
{% include "footer.html" %}

View File

@ -0,0 +1,14 @@
<!-- template for the WFS3 API collections page -->
{% include "header.html" %}
<h1>Collections</h1>
{% for collection in collections %}
<h3><a href="{% for link in links_filter( collection.links, "type", "text/html" ) %}
{{ path_chomp( link.href ) }}
{% endfor %}">{{ collection.title }}</a></h3>
{% endfor %}
{% include "footer.html" %}

View File

@ -0,0 +1,22 @@
<!-- FOOTER TEMPLATE footer.html -->
</div> <!-- //container -->
<footer class="footer bg-light py-4 d-flex flex-column justify-content-around align-items-center">
<div class="container d-flex flex-row justify-content-between align-items-center w-100">
<span><span class="text-muted small mr-2">powered by</span><a class="navbar-brand" href="https://www.qgis.org/" target="_blank">QGIS Server</a></span>
</div>
</footer>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"
integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og=="
crossorigin=""></script>
<script>
jQuery('.jref').jsonFormatter();
</script>
</body>
</html>

View File

@ -0,0 +1,170 @@
<!-- template for the WFS3 API description page -->
{% include "header.html" %}
<h1>API description</h1>
<h2>Info</h2>
<dl class="row">
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">{{ info.description }}</dd>
<dt class="col-sm-3">Title</dt>
<dd class="col-sm-9">{{ info.title }}</dd>
<dt class="col-sm-3">Contact email</dt>
<dd class="col-sm-9">{{ info.contact.email }}</dd>
<dt class="col-sm-3">Contact name</dt>
<dd class="col-sm-9">{{ info.contact.name }}</dd>
</dl>
<h2>Paths</h2>
<div id="accordion">
<div class="card">
{% for path, path_info in paths %}
{% for method, method_data in path_info %}
<div class="card-header" id="heading_{{ method_data.operationId }}">
<h5 class="mb-0">
<span class="badge badge-info">{{ method }}</span>
<button class="btn btn-link" data-toggle="collapse"
data-target="#collapse_{{ method_data.operationId }}"
aria-expanded="true"
aria-controls="collapse_{{ method_data.operationId }}">
{{ path }}
</button>
<span class="small">{{ method_data.summary }}</span>
</h5>
</div>
<div id="collapse_{{ method_data.operationId }}"
class="collapse"
aria-labelledby="heading_{{ method_data.operationId }}"
data-parent="#accordion">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">OperationId</dt>
<dd class="col-sm-9">{{ method_data.operationId }}</dt>
<dt class="col-sm-3">Tags</dt>
<dd class="col-sm-9">{{ method_data.tags }}</dt>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">{{ method_data.description }}</dd>
{% if existsIn(method_data, "parameters") %}
<dt class="col-sm-3">Parameters</dt>
<dd class="col-sm-9">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Type</th>
</tr>
</thead>
<tbody>
{% for param in method_data.parameters %}
{% if existsIn(param, "name") %}
<tr>
<td>{{ param.name }}</td>
<td>{{ param.description }}</td>
<td>{{ param.schema.type }}</td>
</tr>
{% else %}
<tr>
<td colspan=3 class="jref">
{{ param }}
</td>
{% endif %}
{% endfor %}
</tbody>
</table>
</dt>
{% endif %}
<dt class="col-sm-3">Responses</dt>
<dd class="col-sm-9 jref">{{ method_data.responses}}</dd>
</dl>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
<h2>Models</h2>
<div id="accordion">
<div class="card">
{% for schema_name, schema_model in components.schemas %}
<div class="card-header" id="heading_{{ schema_name }}">
<a name="{{ schema_name }}" />
<h5 class="mb-0">
<span class="badge badge-info">{{ schema_name }}</span>
<button class="btn btn-link" data-toggle="collapse"
data-target="#collapse_{{ schema_name }}"
aria-expanded="true"
aria-controls="collapse_{{ schema_name }}">
{{ schema_name }}
</button>
</h5>
</div>
<div id="collapse_{{ schema_name }}"
class="collapse"
aria-labelledby="heading_{{ schema_name }}"
data-parent="#accordion">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Type</dt>
<dd class="col-sm-9">{{ schema_model.type }}</dt>
<dt class="col-sm-3">Properties</dt>
<dd class="col-sm-9">
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Type</th>
<th scope="col">Example</th>
</tr>
</thead>
<tbody>
{% for property_name, property_data in schema_model.properties %}
{% if existsIn(property_data, "example") and existsIn(property_data, "description") %}
<tr>
<td>{{ property_name }}</td>
<td>{{ property_data.description }}</td>
<td>{{ property_data.type }}</td>
<td>
{% if isArray(property_data.example) %}
<ul>
{% for example in property_data.example %}
<li>{{ example }}</li>
{% endfor %}
</ul>
{% else %}
{{ property_data.example }}
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td>{{ property_name }}</td>
<td colspan=3 class="jref">
{{ property_data }}
</td>
{% endif %}
{% endfor %}
</tbody>
</table>
</dt>
<dt class="col-sm-3">Required</dt>
<dd class="col-sm-9">
<ul>
{% for req in schema_model.required %}
<li>{{ req }}</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
{% endfor %}
</div>
</div>
{% include "footer.html" %}

View File

@ -0,0 +1,20 @@
<!-- template for the WFS3 API getFeature page -->
{% include "header.html" %}
<div class="row">
<div class="col-md-6">
<h1>{{ metadata.pageTitle }}</h1>
<dl class="row">
{% for name, value in properties %}
<dt class="col-sm-3">{{ name }}</dt>
<dd class="col-sm-9">{{ value }}</dd>
{% endfor %}
</dl>
</div>
{% include "leaflet_map.html" %}
</div>
{% include "footer.html" %}

View File

@ -0,0 +1,39 @@
<!-- template for the WFS3 API getFeatures page -->
{% include "header.html" %}
<div class="row">
<nav aria-label="Page navigation example">
<ul class="pagination">
{% for link in links_filter( links, "rel", "prev" ) %}
<li class="page-item"><a class="page-link" href="{{ link.href }}">Previous</a></li>
{% endfor %}
<!-- TODO: full pagination: li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li-->
{% for link in links_filter( links, "rel", "next" ) %}
<li class="page-item"><a class="page-link" href="{{ link.href }}">Next</a></li>
{% endfor %}
</ul>
</nav>
</div>
<div class="row">
<div class="col-md-6">
<h1>{{ metadata.pageTitle }}</h1>
{% for feature in features %}
<h2><a href="{{ path_append( feature.id ) }}">{{ metadata.layerTitle }} {{ feature.id }}</a></h2>
<dl class="row">
{% for name, value in feature.properties %}
<dt class="col-sm-3">{{ name }}</dt>
<dd class="col-sm-9">{{ value }}</dd>
{% endfor %}
</dl>
{% endfor %}
</div>
{% include "leaflet_map.html" %}
</div>
{% include "footer.html" %}

View File

@ -0,0 +1,17 @@
<!-- template for the WFS3 API landing page -->
{% include "header.html" %}
<h1>QGIS Server</h1>
<h2>Available services</h2>
<ul>
{% for link in links %}
{% if link.rel != "alternate" %}
{% if link.rel != "self" %}
<li><a rel="{{ link.rel }}" href="{{ link.href }}">{{ link.title }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
{% include "footer.html" %}

View File

@ -0,0 +1,15 @@
<!-- template for the WFS3 API requirement classes -->
{% include "header.html" %}
<h1>QGIS Server</h1>
<h2>Conformance Classes</h2>
<ul>
{% for link in conformsTo %}
<li>{{ link }}</li>
{% endfor %}
</ul>
{% include "footer.html" %}

View File

@ -0,0 +1,45 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/>
<link rel="stylesheet" href="{{ static( "style.css" ) }}" />
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="{{ static( "jsonFormatter.min.css" ) }}" crossorigin="anonymous"></script>
<script src="{{ static( "jsonFormatter.min.js" ) }}" crossorigin="anonymous"></script>
<title>{{ metadata.pageTitle }}</title>
</head>
<body>
<nav class="navbar navbar-light bg-light navbar-expand-sm">
<div class="container">
<div id="navbar" class="navbar-collapse collapse d-flex justify-content-between align-items-center">
<ol class="breadcrumb bg-light my-0 pl-0">
{% for nav in metadata.navigation %}
<li class="breadcrumb-item"><a href="{{ nav.href }}" >{{ nav.title }}</a></li>
{% endfor %}
<li class="breadcrumb-item active">
{{ metadata.pageTitle }}
</li>
</ol>
<ul class="list-unstyled list-separated m-0 p-0 text-muted">
{% for link in links_filter( links, "rel", "alternate" ) %}
<li><a rel="{{ link.rel }}" href="{{ link.href }}" target="_blank">{{ content_type_name( link.type ) }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</nav>
<div class="container pt-4">
<!-- END HEADER TEMPLATE header.html -->

View File

@ -0,0 +1,30 @@
<!-- template for the WFS3 API leaflet map -->
<div class="col-md-6">
<div id="mapid" class="small"></div>
<script type="text/javascript">
jQuery( document ).ready(function( $ ) {
var map = L.map('mapid').setView([0, 0], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map)
$.get( "{{ metadata.geojsonUrl }}", function( data ) {
var jl = L.geoJSON( data, {
onEachFeature: function (feature, layer) {
layer.bindPopup('<h1>'+feature.id +'</h1>');
}
}).addTo(map);
map.setView(jl.getBounds().getCenter());
if ( jl.getBounds().getEast() != jl.getBounds().getWest() && jl.getBounds().getNorth() != jl.getBounds().getSouth() )
{
map.fitBounds(jl.getBounds());
}
function featureOver(e, pos, latlng, data) {
debugger;
}
jl.on('featureOver', featureOver);
});
});
</script>
</div>

View File

@ -0,0 +1,6 @@
<!-- template for the WFS3 API links list -->
<ul>
{% for link in links %}
<li><a rel="{{ link.rel }}" href="{{ link.href }}">{{ link.title }}</a></li>
{% endfor %}
</ul>

View File

@ -405,6 +405,7 @@ sub fix_annotations {
$line =~ s/\bSIP_TRANSFER\b/\/Transfer\//g;
$line =~ s/\bSIP_TRANSFERBACK\b/\/TransferBack\//;
$line =~ s/\bSIP_TRANSFERTHIS\b/\/TransferThis\//;
$line =~ s/\bSIP_GETWRAPPER\b/\/GetWrapper\//;
$line =~ s/SIP_PYNAME\(\s*(\w+)\s*\)/\/PyName=$1\//;
$line =~ s/SIP_TYPEHINT\(\s*(\w+)\s*\)/\/TypeHint="$1"\//;

View File

@ -23,7 +23,7 @@ src/plugins/grass/qtermwidget/
*.*.prepare
*.sld
.agignore
*.json
#Specific files

View File

@ -35,6 +35,13 @@
*/
#define SIP_TRANSFER
/*
* https://www.riverbankcomputing.com/static/Docs/sip/annotations.html#argument-annotation-GetWrapper
*
*/
#define SIP_GETWRAPPER
/*
* http://pyqt.sourceforge.net/Docs/sip4/annotations.html?highlight=keepreference#function-annotation-TransferBack
*/

View File

@ -224,6 +224,11 @@ json QgsJsonExporter::exportFeatureToJsonObject( const QgsFeature &feature, cons
}
QString QgsJsonExporter::exportFeatures( const QgsFeatureList &features, int indent ) const
{
return QString::fromStdString( exportFeaturesToJsonObject( features ).dump( indent ) );
}
json QgsJsonExporter::exportFeaturesToJsonObject( const QgsFeatureList &features ) const
{
json data
{
@ -235,7 +240,7 @@ QString QgsJsonExporter::exportFeatures( const QgsFeatureList &features, int ind
{
data["features"].push_back( exportFeatureToJsonObject( feature ) );
}
return QString::fromStdString( data.dump( indent ) );
return data;
}
//

View File

@ -213,7 +213,7 @@ class CORE_EXPORT QgsJsonExporter
* \param extraProperties map of extra attributes to include in feature's properties
* \param id optional ID to use as GeoJSON feature's ID instead of input feature's ID. If omitted, feature's
* ID is used.
* \returns QJsonObject
* \returns json object
* \see exportFeatures()
*/
json exportFeatureToJsonObject( const QgsFeature &feature,
@ -230,6 +230,15 @@ class CORE_EXPORT QgsJsonExporter
*/
QString exportFeatures( const QgsFeatureList &features, int indent = -1 ) const;
/**
* Returns a JSON object representation of a list of features (feature collection).
* \param features features to convert
* \returns json object
* \see exportFeatures()
* \since QGIS 3.10
*/
json exportFeaturesToJsonObject( const QgsFeatureList &features ) const SIP_SKIP;
private:
//! Maximum number of decimal places for geometry coordinates

View File

@ -174,6 +174,11 @@ const QgsDataProvider *QgsMapLayer::dataProvider() const
return nullptr;
}
QString QgsMapLayer::shortName() const
{
return mShortName;
}
QString QgsMapLayer::publicSource() const
{
// Redo this every time we're asked for it, as we don't know if

View File

@ -261,7 +261,7 @@ class CORE_EXPORT QgsMapLayer : public QObject
* used by QGIS Server to identify the layer.
* \see setShortName()
*/
QString shortName() const { return mShortName; }
QString shortName() const;
/**
* Sets the title of the layer

View File

@ -2715,6 +2715,25 @@ QList<QgsMapLayer *> QgsProject::mapLayersByName( const QString &layerName ) con
return mLayerStore->mapLayersByName( layerName );
}
QList<QgsMapLayer *> QgsProject::mapLayersByShortName( const QString &shortName ) const
{
QList<QgsMapLayer *> layers;
const auto constMapLayers { mLayerStore->mapLayers() };
for ( const auto &l : constMapLayers )
{
if ( ! l->shortName().isEmpty() )
{
if ( l->shortName() == shortName )
layers << l;
}
else if ( l->name() == shortName )
{
layers << l;
}
}
return layers;
}
bool QgsProject::unzip( const QString &filename, QgsProject::ReadFlags flags )
{
clearError();

View File

@ -759,6 +759,18 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
QList<QgsMapLayer *> mapLayersByName( const QString &layerName ) const;
/**
* Retrieves a list of matching registered layers by layer \a shortName.
* If layer's short name is empty a match with layer's name is attempted.
*
* \returns list of matching layers
* \see mapLayer()
* \see mapLayers()
* \since QGIS 3.10
*/
QList<QgsMapLayer *> mapLayersByShortName( const QString &shortName ) const;
/**
* Returns a map of all registered layers by layer ID.
*
@ -792,6 +804,38 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
{
return mLayerStore->layers<T>();
}
/**
* Retrieves a list of matching registered layers by layer \a shortName with a specified layer type,
* if layer's short name is empty a match with layer's name is attempted.
*
* \param shortName short name of layers to match
* \returns list of matching layers
* \see mapLayer()
* \see mapLayers()
* \note not available in Python bindings
* \since QGIS 3.10
*/
template <typename T>
QVector<T> mapLayersByShortName( const QString &shortName ) const
{
QVector<T> layers;
const auto constMapLayers { mLayerStore->layers<T>() };
for ( const auto l : constMapLayers )
{
if ( ! l->shortName().isEmpty() )
{
if ( l->shortName() == shortName )
layers << l;
}
else if ( l->name() == shortName )
{
layers << l;
}
}
return layers;
}
#endif
/**

View File

@ -218,7 +218,7 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c
}
QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig )
QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig )
{
QString uri;
if ( type == QLatin1String( "HTTP/HTTPS/FTP" ) )

View File

@ -26,6 +26,11 @@ SET(QGIS_SERVER_SRCS
qgsfilterrestorer.cpp
qgsrequesthandler.cpp
qgsserver.cpp
qgsserverapi.cpp
qgsserverogcapi.cpp
qgsserverogcapihandler.cpp
qgsserverapiutils.cpp
qgsserverapicontext.cpp
qgsserverparameters.cpp
qgsserverexception.cpp
qgsserverinterface.cpp
@ -42,11 +47,17 @@ SET(QGIS_SERVER_SRCS
qgsfeaturefilterprovidergroup.cpp
qgsfeaturefilter.cpp
qgsstorebadlayerinfo.cpp
qgsserverquerystringparameter.cpp
)
SET (QGIS_SERVER_HDRS
qgsservicemodule.h
qgsmapserviceexception.h
qgsserverapi.h
qgsserverogcapi.h
qgsserverogcapihandler.h
qgsserverapicontext.h
qgsserverquerystringparameter.h
)
@ -54,8 +65,10 @@ SET (QGIS_SERVER_MOC_HDRS
qgscapabilitiescache.h
qgsconfigcache.h
qgsserverlogger.h
qgsserverogcapi.h
qgsserversettings.h
qgsserverparameters.h
qgsserverquerystringparameter.h
)
@ -99,6 +112,7 @@ INCLUDE_DIRECTORIES(SYSTEM
${QTKEYCHAIN_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/auth
${CMAKE_SOURCE_DIR}/src/core/dxf

View File

@ -117,11 +117,18 @@ QgsFcgiServerRequest::QgsFcgiServerRequest()
setUrl( url );
setMethod( method );
// Get accept header for content-type negotiation
const char *accept = getenv( "HTTP_ACCEPT" );
if ( accept )
{
setHeader( QStringLiteral( "Accept" ), accept );
}
// Output debug infos
Qgis::MessageLevel logLevel = QgsServerLogger::instance()->logLevel();
if ( logLevel <= Qgis::Info )
{
printRequestInfos();
printRequestInfos( url );
}
}
@ -175,7 +182,7 @@ void QgsFcgiServerRequest::readData()
}
}
void QgsFcgiServerRequest::printRequestInfos()
void QgsFcgiServerRequest::printRequestInfos( const QUrl &url )
{
QgsMessageLog::logMessage( QStringLiteral( "******************** New request ***************" ), QStringLiteral( "Server" ), Qgis::Info );
@ -183,18 +190,29 @@ void QgsFcgiServerRequest::printRequestInfos()
{
QStringLiteral( "SERVER_NAME" ),
QStringLiteral( "REQUEST_URI" ),
QStringLiteral( "SCRIPT_NAME" ),
QStringLiteral( "HTTPS" ),
QStringLiteral( "REMOTE_ADDR" ),
QStringLiteral( "REMOTE_HOST" ),
QStringLiteral( "SERVER_PORT" ),
QStringLiteral( "QUERY_STRING" ),
QStringLiteral( "REMOTE_USER" ),
QStringLiteral( "REMOTE_IDENT" ),
QStringLiteral( "CONTENT_TYPE" ),
QStringLiteral( "REQUEST_METHOD" ),
QStringLiteral( "AUTH_TYPE" ),
QStringLiteral( "HTTP_ACCEPT" ),
QStringLiteral( "HTTP_USER_AGENT" ),
QStringLiteral( "HTTP_PROXY" ),
QStringLiteral( "NO_PROXY" ),
QStringLiteral( "HTTP_AUTHORIZATION" )
QStringLiteral( "HTTP_AUTHORIZATION" ),
QStringLiteral( "QGIS_PROJECT_FILE" )
};
QgsMessageLog::logMessage( QStringLiteral( "Request URL: %2" ).arg( url.url() ), QStringLiteral( "Server" ), Qgis::Info );
QgsMessageLog::logMessage( QStringLiteral( "Environment:" ), QStringLiteral( "Server" ), Qgis::Info );
QgsMessageLog::logMessage( QStringLiteral( "------------------------------------------------" ), QStringLiteral( "Server" ), Qgis::Info );
for ( const auto &envVar : envVars )
{
if ( getenv( envVar.toStdString().c_str() ) )

View File

@ -46,7 +46,7 @@ class SERVER_EXPORT QgsFcgiServerRequest: public QgsServerRequest
// Log request info: print debug infos
// about the request
void printRequestInfos();
void printRequestInfos( const QUrl &url );
QByteArray mData;

View File

@ -18,6 +18,8 @@
#ifndef QGSMAPSERVICEEXCEPTION
#define QGSMAPSERVICEEXCEPTION
#define SIP_NO_FILE
#include <QString>
#include "qgsserverexception.h"
@ -36,7 +38,6 @@
* * "OperationNotSupported"
* \deprecated Use QsgServerException
*/
class SERVER_EXPORT QgsMapServiceException : public QgsOgcServiceException
{
public:

View File

@ -34,6 +34,8 @@
#include "qgsserverrequest.h"
#include "qgsfilterresponsedecorator.h"
#include "qgsservice.h"
#include "qgsserverapi.h"
#include "qgsserverapicontext.h"
#include "qgsserverparameters.h"
#include "qgsapplication.h"
@ -145,7 +147,16 @@ QString QgsServer::configPath( const QString &defaultConfigPath, const QString &
{
if ( configPath.isEmpty() )
{
QgsMessageLog::logMessage( QStringLiteral( "Using default configuration file path: %1" ).arg( defaultConfigPath ), QStringLiteral( "Server" ), Qgis::Info );
// Read it from the environment, because a rewrite rule may have rewritten it
if ( getenv( "QGIS_PROJECT_FILE" ) )
{
cfPath = getenv( "QGIS_PROJECT_FILE" );
QgsMessageLog::logMessage( QStringLiteral( "Using configuration file path from environment: %1" ).arg( cfPath ), QStringLiteral( "Server" ), Qgis::Info );
}
else if ( ! defaultConfigPath.isEmpty() )
{
QgsMessageLog::logMessage( QStringLiteral( "Using default configuration file path: %1" ).arg( defaultConfigPath ), QStringLiteral( "Server" ), Qgis::Info );
}
}
else
{
@ -344,33 +355,54 @@ void QgsServer::handleRequest( QgsServerRequest &request, QgsServerResponse &res
const QgsServerParameters params = request.serverParameters();
printRequestParameters( params.toMap(), logLevel );
//Config file path
// Setup project (config file path)
if ( ! project )
{
QString configFilePath = configPath( *sConfigFilePath, params.map() );
// load the project if needed and not empty
project = mConfigCache->project( sServerInterface->configFilePath() );
project = mConfigCache->project( configFilePath );
}
if ( project )
{
sServerInterface->setConfigFilePath( project->fileName() );
}
// Dispatcher: if SERVICE is set, we assume a OWS service, if not, let's try an API
// TODO: QGIS 4 fix the OWS services and treat them as APIs
QgsServerApi *api = nullptr;
if ( params.service().isEmpty() && ( api = sServiceRegistry->apiForRequest( request ) ) )
{
QgsServerApiContext context { api->rootPath(), &request, &responseDecorator, project, sServerInterface };
api->executeRequest( context );
}
else
{
// Project is mandatory for OWS at this point
if ( ! project )
{
throw QgsServerException( QStringLiteral( "Project file error" ) );
}
}
if ( ! params.fileName().isEmpty() )
{
const QString value = QString( "attachment; filename=\"%1\"" ).arg( params.fileName() );
requestHandler.setResponseHeader( QStringLiteral( "Content-Disposition" ), value );
}
if ( ! params.fileName().isEmpty() )
{
const QString value = QString( "attachment; filename=\"%1\"" ).arg( params.fileName() );
requestHandler.setResponseHeader( QStringLiteral( "Content-Disposition" ), value );
}
// Lookup for service
QgsService *service = sServiceRegistry->getService( params.service(), params.version() );
if ( service )
{
service->executeRequest( request, responseDecorator, project );
}
else
{
throw QgsOgcServiceException( QStringLiteral( "Service configuration error" ),
QStringLiteral( "Service unknown or unsupported" ) );
// Lookup for service
QgsService *service = sServiceRegistry->getService( params.service(), params.version() );
if ( service )
{
service->executeRequest( request, responseDecorator, project );
}
else
{
throw QgsOgcServiceException( QStringLiteral( "Service configuration error" ),
QStringLiteral( "Service unknown or unsupported" ) );
}
}
}
catch ( QgsServerException &ex )

View File

@ -84,7 +84,7 @@ class SERVER_EXPORT QgsServer
/**
* Initialize Python
* Note: not in Python bindings
* \note not available in Python bindings
*/
void initPython();
#endif

View File

@ -0,0 +1,37 @@
/***************************************************************************
qgsserverapi.cpp
Class defining the service interface for QGIS server APIs.
-------------------
begin : 2019-04-16
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 "qgsserverapi.h"
QgsServerApi::QgsServerApi( QgsServerInterface *serverIface )
: mServerIface( serverIface )
{
}
bool QgsServerApi::accept( const QUrl &url ) const
{
return url.path().contains( rootPath() );
}
QgsServerInterface *QgsServerApi::serverIface() const
{
return mServerIface;
}

141
src/server/qgsserverapi.h Normal file
View File

@ -0,0 +1,141 @@
/***************************************************************************
qgsserverapi.h
Class defining the service interface for QGIS server APIs.
-------------------
begin : 2019-04-16
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 QGSSERVERAPI_H
#define QGSSERVERAPI_H
#include "qgis_server.h"
#include <QRegularExpression>
#include "qgsserverexception.h"
#include "qgsserverrequest.h"
class QgsServerResponse;
class QgsProject;
class QgsServerApiContext;
class QgsServerInterface;
/**
* \ingroup server
* Server generic API endpoint abstract base class.
*
* \see QgsServerOgcApi for an OGC API (aka WFS3) implementation.
*
* An API must have a name and a (possibly empty) version and define a
* (possibly empty) root path (e.g. "/wfs3").
*
* The server routing logic will check incoming request URLs by passing them
* to the API's accept(url) method, the default implementation performs a simple
* check for the presence of the API's root path string in the URL.
* This simple logic implies that APIs must be registered in reverse order from the
* most specific to the most generic: given two APIs with root paths '/wfs' and '/wfs3',
* '/wfs3' must be registered first or it will be shadowed by '/wfs'.
* APIs developers are encouraged to implement a more robust accept(url) logic by
* making sure that their APIs accept only URLs they can actually handle, if they do,
* the APIs registration order becomes irrelevant.
*
* After the API has been registered to the server API registry:
*
* \code{.py}
* class API(QgsServerApi):
*
* def name(self):
* return "Test API"
*
* def rootPath(self):
* return "/testapi"
*
* def executeRequest(self, request_context):
* request_context.response().write(b"\"Test API\"")
*
* server = QgsServer()
* api = API(server.serverInterface())
* server.serverInterface().serviceRegistry().registerApi(api)
* \endcode
*
* the incoming calls with an URL path starting with the API root path
* will be routed to the first matching API and executeRequest() method
* of the API will be invoked.
*
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApi
{
public:
/**
* Creates a QgsServerApi object
*/
QgsServerApi( QgsServerInterface *serverIface );
virtual ~QgsServerApi() = default;
/**
* Returns the API name
*/
virtual const QString name() const = 0;
/**
* Returns the API description
*/
virtual const QString description() const = 0;
/**
* Returns the version of the service
* \note the default implementation returns an empty string
*/
virtual const QString version() const { return QString(); }
/**
* Returns the root path for the API
*/
virtual const QString rootPath() const = 0;
/**
* Returns TRUE if the given method is supported by the API, default implementation supports all methods.
*/
virtual bool allowMethod( QgsServerRequest::Method ) const { return true; }
/**
* Returns TRUE if the given \a url is handled by the API, default implementation checks for the presence of rootPath inside the \a url path.
*/
virtual bool accept( const QUrl &url ) const;
/**
* Executes a request by passing the given \a context to the API handlers.
*/
virtual void executeRequest( const QgsServerApiContext &context ) const = 0;
/**
* Returns the server interface
*/
QgsServerInterface *serverIface() const;
private:
QgsServerInterface *mServerIface = nullptr;
};
#endif // QGSSERVERAPI_H

View File

@ -0,0 +1,83 @@
/***************************************************************************
qgsserverapicontext.cpp - QgsServerApiContext
---------------------
begin : 13.5.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 "qgsserverapicontext.h"
#include "qgsserverrequest.h"
#include "qgsserverresponse.h"
#include "qgsproject.h"
#include "qgsserverinterface.h"
QgsServerApiContext::QgsServerApiContext( const QString &apiRootPath, const QgsServerRequest *request, QgsServerResponse *response, const QgsProject *project, QgsServerInterface *serverInterface ):
mApiRootPath( apiRootPath ),
mRequest( request ),
mResponse( response ),
mProject( project ),
mServerInterface( serverInterface )
{
}
const QgsServerRequest *QgsServerApiContext::request() const
{
return mRequest;
}
QgsServerResponse *QgsServerApiContext::response() const
{
return mResponse;
}
const QgsProject *QgsServerApiContext::project() const
{
return mProject;
}
void QgsServerApiContext::setProject( const QgsProject *project )
{
mProject = project;
}
QgsServerInterface *QgsServerApiContext::serverInterface() const
{
return mServerInterface;
}
const QString QgsServerApiContext::matchedPath() const
{
auto path { mRequest->url().path( )};
const int idx { path.indexOf( mApiRootPath )};
if ( idx != -1 )
{
path.truncate( idx + mApiRootPath.length() );
return path;
}
else
{
return QString();
}
}
QString QgsServerApiContext::apiRootPath() const
{
return mApiRootPath;
}
void QgsServerApiContext::setRequest( const QgsServerRequest *request )
{
mRequest = request;
}

View File

@ -0,0 +1,113 @@
/***************************************************************************
qgsserverapicontext.h - QgsServerApiContext
---------------------
begin : 13.5.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 QGSSERVERAPICONTEXT_H
#define QGSSERVERAPICONTEXT_H
#include "qgis_server.h"
#include <QString>
class QgsServerResponse;
class QgsServerRequest;
class QgsServerInterface;
class QgsProject;
/**
* \ingroup server
* The QgsServerApiContext class encapsulates the resources for a particular client
* request: the request and response objects, the project (might be NULL) and
* the server interface, the API root path that matched the request is also added.
*
* QgsServerApiContext is lightweight copyable object meant to be passed along the
* request handlers chain.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiContext
{
public:
/**
* QgsServerApiContext constructor
*
* \param apiRootPath is the API root path, this information is used by the
* handlers to build the href links to the resources and to the HTML templates.
* \param request the incoming request
* \param response the response
* \param project the project (might be NULL)
* \param serverInterface the server interface
*/
QgsServerApiContext( const QString &apiRootPath, const QgsServerRequest *request, QgsServerResponse *response,
const QgsProject *project, QgsServerInterface *serverInterface );
/**
* Returns the server request object
*/
const QgsServerRequest *request() const;
/**
* Returns the server response object
*/
QgsServerResponse *response() const;
/**
* Returns the (possibly NULL) project
* \see setProject()
*/
const QgsProject *project() const;
/**
* Sets the project to \a project
* \see project()
*/
void setProject( const QgsProject *project );
/**
* Returns the server interface
*/
QgsServerInterface *serverInterface() const;
/**
* Returns the initial part of the incoming request URL path that matches the
* API root path.
* If there is no match returns an empty string (it should never happen).
*
* I.e. for an API with root path "/wfs3" and an incoming request
* "https://www.qgis.org/services/wfs3/collections"
* this method will return "/resources/wfs3"
*
*/
const QString matchedPath( ) const;
/**
* Returns the API root path
*/
QString apiRootPath() const;
/**
* Sets context request to \a request
*/
void setRequest( const QgsServerRequest *request );
private:
QString mApiRootPath;
const QgsServerRequest *mRequest = nullptr;
QgsServerResponse *mResponse = nullptr;
const QgsProject *mProject = nullptr;
QgsServerInterface *mServerInterface = nullptr;
};
#endif // QGSSERVERAPICONTEXT_H

View File

@ -0,0 +1,196 @@
/***************************************************************************
qgsserverapiutils.cpp
Class defining utilities for QGIS server APIs.
-------------------
begin : 2019-04-16
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 "qgsserverapiutils.h"
#include "qgsrectangle.h"
#include "qgsvectorlayer.h"
#include "qgscoordinatereferencesystem.h"
#include "qgsserverprojectutils.h"
#include "qgsmessagelog.h"
#include "nlohmann/json.hpp"
#include <QUrl>
QgsRectangle QgsServerApiUtils::parseBbox( const QString &bbox )
{
const auto parts { bbox.split( ',', QString::SplitBehavior::SkipEmptyParts ) };
// Note: Z is ignored
auto ok { true };
if ( parts.count() == 4 || parts.count() == 6 )
{
const auto hasZ { parts.count() == 6 };
auto toDouble = [ & ]( const int i ) -> double
{
if ( ! ok )
return 0;
return parts[i].toDouble( &ok );
};
QgsRectangle rect;
if ( hasZ )
{
rect = QgsRectangle( toDouble( 0 ), toDouble( 1 ),
toDouble( 3 ), toDouble( 4 ) );
}
else
{
rect = QgsRectangle( toDouble( 0 ), toDouble( 1 ),
toDouble( 2 ), toDouble( 3 ) );
}
if ( ok )
{
return rect;
}
}
return QgsRectangle();
}
json QgsServerApiUtils::layerExtent( const QgsVectorLayer *layer )
{
auto extent { layer->extent() };
if ( layer->crs().postgisSrid() != 4326 )
{
static const QgsCoordinateReferenceSystem targetCrs { 4326 };
const QgsCoordinateTransform ct( layer->crs(), targetCrs, layer->transformContext() );
extent = ct.transform( extent );
}
return {{ extent.xMinimum(), extent.yMinimum(), extent.xMaximum(), extent.yMaximum() }};
}
QgsCoordinateReferenceSystem QgsServerApiUtils::parseCrs( const QString &bboxCrs )
{
QgsCoordinateReferenceSystem crs;
// We get this:
// http://www.opengis.net/def/crs/OGC/1.3/CRS84
// We want this:
// "urn:ogc:def:crs:<auth>:[<version>]:<code>"
const auto parts { QUrl( bboxCrs ).path().split( '/' ) };
if ( parts.count() == 6 )
{
return crs.fromOgcWmsCrs( QStringLiteral( "urn:ogc:def:crs:%1:%2:%3" ).arg( parts[3], parts[4], parts[5] ) );
}
else
{
return crs;
}
}
const QgsFields QgsServerApiUtils::publishedFields( const QgsVectorLayer *layer )
{
// TODO: implement plugin's ACL filtering
return layer->fields();
}
const QVector<QgsMapLayer *> QgsServerApiUtils::publishedWfsLayers( const QgsProject *project )
{
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
const QStringList wfstUpdateLayersId = QgsServerProjectUtils::wfstUpdateLayerIds( *project );
const QStringList wfstInsertLayersId = QgsServerProjectUtils::wfstInsertLayerIds( *project );
const QStringList wfstDeleteLayersId = QgsServerProjectUtils::wfstDeleteLayerIds( *project );
QVector<QgsMapLayer *> result;
const auto constLayers { project->mapLayers() };
for ( auto it = project->mapLayers().constBegin(); it != project->mapLayers().constEnd(); it++ )
{
if ( wfstUpdateLayersId.contains( it.value()->id() ) ||
wfstInsertLayersId.contains( it.value()->id() ) ||
wfstDeleteLayersId.contains( it.value()->id() ) )
{
result.push_back( it.value() );
}
}
return result;
}
QString QgsServerApiUtils::sanitizedFieldValue( const QString &value )
{
QString result { QUrl( value ).toString() };
static const QRegularExpression re( R"raw(;.*(DROP|DELETE|INSERT|UPDATE|CREATE|INTO))raw" );
if ( re.match( result.toUpper() ).hasMatch() )
{
result = QString();
}
return result.replace( '\'', QStringLiteral( "\'" ) );
}
QStringList QgsServerApiUtils::publishedCrsList( const QgsProject *project )
{
// This must be always available in OGC APIs
QStringList result { { QStringLiteral( "http://www.opengis.net/def/crs/OGC/1.3/CRS84" )}};
if ( project )
{
const QStringList outputCrsList = QgsServerProjectUtils::wmsOutputCrsList( *project );
for ( const QString &crsId : outputCrsList )
{
const auto crsUri { crsToOgcUri( QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsId ) ) };
if ( ! crsUri.isEmpty() )
{
result.push_back( crsUri );
}
}
}
return result;
}
QString QgsServerApiUtils::crsToOgcUri( const QgsCoordinateReferenceSystem &crs )
{
const auto parts { crs.authid().split( ':' ) };
if ( parts.length() == 2 )
{
if ( parts[0] == QStringLiteral( "EPSG" ) )
return QStringLiteral( "http://www.opengis.net/def/crs/EPSG/9.6.2/%1" ).arg( parts[1] ) ;
else if ( parts[0] == QStringLiteral( "OGC" ) )
{
return QStringLiteral( "http://www.opengis.net/def/crs/OGC/1.3/%1" ).arg( parts[1] ) ;
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URI %1: (not OGC or EPSG)" ).arg( crs.authid() ), QStringLiteral( "Server" ), Qgis::Critical );
}
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "Error converting published CRS to URI: %1" ).arg( crs.authid() ), QStringLiteral( "Server" ), Qgis::Critical );
}
return QString();
}
QString QgsServerApiUtils::appendMapParameter( const QString &path, const QUrl &requestUrl )
{
QList<QPair<QString, QString> > qi;
QString result { path };
const auto constItems { requestUrl.queryItems( ) };
for ( const auto &i : constItems )
{
if ( i.first.compare( QStringLiteral( "MAP" ), Qt::CaseSensitivity::CaseInsensitive ) == 0 )
{
qi.push_back( i );
}
}
if ( ! qi.empty() )
{
if ( ! path.endsWith( '?' ) )
{
result += '?';
}
result.append( QStringLiteral( "MAP=%1" ).arg( qi.first().second ) );
}
return result;
}

View File

@ -0,0 +1,147 @@
/***************************************************************************
qgsserverapiutils.h
Class defining utilities for QGIS server APIs.
-------------------
begin : 2019-04-16
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 QGSSERVERAPIUTILS_H
#define QGSSERVERAPIUTILS_H
#include "qgis_server.h"
#include <QString>
#include "qgsproject.h"
#include "qgsserverprojectutils.h"
class QgsRectangle;
class QgsCoordinateReferenceSystem;
class QgsVectorLayer;
#ifndef SIP_RUN
#include "nlohmann/json_fwd.hpp"
using json = nlohmann::json;
#endif
/**
* \ingroup server
* The QgsServerApiUtils class contains helper functions to handle common API operations.
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiUtils
{
public:
/**
* Parses a comma separated \a bbox into a (possibily empty) QgsRectangle.
*
* \note Z values (i.e. a 6 elements bbox) are silently discarded
*/
static QgsRectangle parseBbox( const QString &bbox );
/**
* layerExtent returns json array with [xMin,yMin,xMax,yMax] CRS84 extent for the given \a layer
* FIXME: the OpenAPI swagger docs say that it is inverted axis order: West, north, east, south edges of the spatial extent.
* but current example implementations and GDAL assume it's not.
* TODO: maybe consider advertised extent instead?
*/
static json layerExtent( const QgsVectorLayer *layer ) SIP_SKIP;
/**
* Parses the CRS URI \a bboxCrs (example: "http://www.opengis.net/def/crs/OGC/1.3/CRS84") into a QGIS CRS object
*/
static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs );
/**
* Returns the list of fields accessible to the service for a given \a layer.
*
* This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
* TODO: implement ACL
*/
static const QgsFields publishedFields( const QgsVectorLayer *layer );
/**
* Returns the list of layers accessible to the service for a given \a project.
*
* This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
*
* \note project must not be NULL
* TODO: implement ACL
*/
static const QVector<QgsMapLayer *> publishedWfsLayers( const QgsProject *project );
#ifndef SIP_RUN
/**
* Returns the list of layers of type T accessible to the WFS service for a given \a project.
*
* Example:
*
* QVector<QgsVectorLayer*> vectorLayers = publishedLayers<QgsVectorLayer>();
*
* TODO: implement ACL
* \note not available in Python bindings
* \see publishedWfsLayers()
*/
template <typename T>
static const QVector<T *> publishedWfsLayers( const QgsProject *project )
{
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
const QStringList wfstUpdateLayersId = QgsServerProjectUtils::wfstUpdateLayerIds( *project );
const QStringList wfstInsertLayersId = QgsServerProjectUtils::wfstInsertLayerIds( *project );
const QStringList wfstDeleteLayersId = QgsServerProjectUtils::wfstDeleteLayerIds( *project );
QVector<T *> result;
const auto constLayers { project->layers<T *>() };
for ( const auto &layer : constLayers )
{
if ( wfstUpdateLayersId.contains( layer->id() ) ||
wfstInsertLayersId.contains( layer->id() ) ||
wfstDeleteLayersId.contains( layer->id() ) )
{
result.push_back( layer );
}
}
return result;
}
#endif
/**
* Sanitizes the input \a value by removing URL encoding and checking for malicious content.
* In case of failure returns an empty string.
*/
static QString sanitizedFieldValue( const QString &value );
/**
* Returns the list of CRSs (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84) available for this \a project.
* Information is read from project WMS configuration.
*/
static QStringList publishedCrsList( const QgsProject *project );
/**
* Returns a \a crs as OGC URI (format: http://www.opengis.net/def/crs/OGC/1.3/CRS84)
* Returns an empty string on failure.
*/
static QString crsToOgcUri( const QgsCoordinateReferenceSystem &crs );
/**
* Appends MAP query string parameter from current \a requestUrl to the given \a path
*/
static QString appendMapParameter( const QString &path, const QUrl &requestUrl );
};
#endif // QGSSERVERAPIUTILS_H

View File

@ -18,12 +18,18 @@
#ifndef QGSSERVEREXCEPTION_H
#define QGSSERVEREXCEPTION_H
#include <QString>
#include <QByteArray>
#include "qgsexception.h"
#include "qgis_server.h"
#include "qgis_sip.h"
#include "nlohmann/json.hpp"
#ifndef SIP_RUN
using json = nlohmann::json;
#endif
/**
@ -128,4 +134,183 @@ class SERVER_EXPORT QgsBadRequestException: public QgsOgcServiceException
};
#endif
#ifndef SIP_RUN // No API exceptions for SIP, see python/server/qgsserverexception.sip
/**
* \ingroup server
* \class QgsServerApiException
* \brief Exception base class for API exceptions.
*
* Note that this exception is associated with a default return code 200 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiException: public QgsServerException
{
public:
//! Construction
QgsServerApiException( const QString &code, const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 200 )
: QgsServerException( message, responseCode )
, mCode( code )
, mMimeType( mimeType )
{
}
QByteArray formatResponse( QString &responseFormat SIP_OUT ) const override
{
responseFormat = mMimeType;
json data
{
{
{ "code", mCode.toStdString() },
{ "description", what().toStdString() },
}
};
if ( responseFormat == QStringLiteral( "application/json" ) )
{
return QByteArray::fromStdString( data.dump() );
}
else if ( responseFormat == QStringLiteral( "text/html" ) )
{
// TODO: template
return QByteArray::fromStdString( data.dump() );
}
else
{
// TODO: template
return QByteArray::fromStdString( data.dump() );
}
}
private:
QString mCode;
QString mMimeType;
};
/**
* \ingroup server
* \class QgsServerApiInternalServerError
* \brief Internal server error API exception.
*
* Note that this exception is associated with a default return code 500 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiInternalServerError: public QgsServerApiException
{
public:
//! Construction
QgsServerApiInternalServerError( const QString &message = QStringLiteral( "Internal server error" ), const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 500 )
: QgsServerApiException( QStringLiteral( "Internal server error" ), message, mimeType, responseCode )
{
}
};
/**
* \ingroup server
* \class QgsServerApiNotFoundError
* \brief Not found error API exception.
*
* Note that this exception is associated with a default return code 404 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiNotFoundError: public QgsServerApiException
{
public:
//! Construction
QgsServerApiNotFoundError( const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 404 )
: QgsServerApiException( QStringLiteral( "API not found error" ), message, mimeType, responseCode )
{
}
};
/**
* \ingroup server
* \class QgsServerApiBadRequestException
* \brief Bad request error API exception.
*
* Note that this exception is associated with a default return code 400 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiBadRequestException: public QgsServerApiException
{
public:
//! Construction
QgsServerApiBadRequestException( const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 400 )
: QgsServerApiException( QStringLiteral( "Bad request error" ), message, mimeType, responseCode )
{
}
};
/**
* \ingroup server
* \class QgsServerApiImproperlyConfiguredException
* \brief configuration error on the server prevents to serve the request, which would be valid otherwise.
*
* Note that this exception is associated with a default return code 500 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiImproperlyConfiguredException: public QgsServerApiException
{
public:
//! Construction
QgsServerApiImproperlyConfiguredException( const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 500 )
: QgsServerApiException( QStringLiteral( "Improperly configured error" ), message, mimeType, responseCode )
{
}
};
/**
* \ingroup server
* \class QgsServerApiNotImplementedException
* \brief this method is not yet implemented
*
* Note that this exception is associated with a default return code 500 which may be
* not appropriate in some situations.
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiNotImplementedException: public QgsServerApiException
{
public:
//! Construction
QgsServerApiNotImplementedException( const QString &message = QStringLiteral( "Requested method is not implemented" ), const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 500 )
: QgsServerApiException( QStringLiteral( "Not implemented error" ), message, mimeType, responseCode )
{
}
};
/**
* \ingroup server
* \class QgsServerApiInvalidMimeTypeException
* \brief the client sent an invalid mime type in the "Accept" header
*
* Note that this exception is associated with a default return code 406
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerApiInvalidMimeTypeException: public QgsServerApiException
{
public:
//! Construction
QgsServerApiInvalidMimeTypeException( const QString &message = QStringLiteral( "The Accept header submitted in the request did not support any of the media types supported by the server for the requested resource" ), const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 406 )
: QgsServerApiException( QStringLiteral( "Invalid mime-type" ), message, mimeType, responseCode )
{
}
};
#endif // no API exceptions for SIP
#endif

View File

@ -0,0 +1,152 @@
/***************************************************************************
qgsserverogcapi.cpp - QgsServerOgcApi
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 <QDir>
#include <QDebug>
#include "qgsserverogcapi.h"
#include "qgsserverogcapihandler.h"
#include "qgsmessagelog.h"
#include "qgsapplication.h"
QMap<QgsServerOgcApi::ContentType, QString> QgsServerOgcApi::sContentTypeMime = [ ]() -> QMap<QgsServerOgcApi::ContentType, QString>
{
QMap<QgsServerOgcApi::ContentType, QString> map;
map[QgsServerOgcApi::ContentType::JSON] = QStringLiteral( "application/json" );
map[QgsServerOgcApi::ContentType::GEOJSON] = QStringLiteral( "application/geo+json" );
map[QgsServerOgcApi::ContentType::HTML] = QStringLiteral( "text/html" );
map[QgsServerOgcApi::ContentType::OPENAPI3] = QStringLiteral( "application/openapi+json;version=3.0" );
return map;
}();
QHash<QgsServerOgcApi::ContentType, QList<QgsServerOgcApi::ContentType>> QgsServerOgcApi::sContentTypeAliases = [ ]() -> QHash<ContentType, QList<ContentType>>
{
QHash<QgsServerOgcApi::ContentType, QList<QgsServerOgcApi::ContentType>> map;
map[ContentType::JSON] = { QgsServerOgcApi::ContentType::GEOJSON, QgsServerOgcApi::ContentType::OPENAPI3 };
return map;
}();
QgsServerOgcApi::QgsServerOgcApi( QgsServerInterface *serverIface, const QString &rootPath, const QString &name, const QString &description, const QString &version ):
QgsServerApi( serverIface ),
mRootPath( rootPath ),
mName( name ),
mDescription( description ),
mVersion( version )
{
}
QgsServerOgcApi::~QgsServerOgcApi()
{
//qDebug() << "API destroyed: " << name();
}
void QgsServerOgcApi::registerHandler( QgsServerOgcApiHandler *handler )
{
std::shared_ptr<QgsServerOgcApiHandler> hp( handler );
mHandlers.emplace_back( std::move( hp ) );
}
QUrl QgsServerOgcApi::sanitizeUrl( const QUrl &url )
{
return url.adjusted( QUrl::StripTrailingSlash | QUrl::NormalizePathSegments );
}
void QgsServerOgcApi::executeRequest( const QgsServerApiContext &context ) const
{
// Get url
auto path { sanitizeUrl( context.request()->url() ).path() };
//path.truncate( context.apiRootPath().length() );
// Find matching handler
auto hasMatch { false };
for ( const auto &h : mHandlers )
{
QgsMessageLog::logMessage( QStringLiteral( "Checking API path %1 for %2 " ).arg( path, h->path().pattern() ), QStringLiteral( "Server" ), Qgis::Info );
if ( h->path().match( path ).hasMatch() )
{
hasMatch = true;
// Execute handler
QgsMessageLog::logMessage( QStringLiteral( "Found API handler %1" ).arg( QString::fromStdString( h->operationId() ) ), QStringLiteral( "Server" ), Qgis::Info );
// May throw QgsServerApiBadRequestException or JSON exceptions on serializing
try
{
h->handleRequest( context );
}
catch ( json::exception &ex )
{
throw QgsServerApiInternalServerError( QStringLiteral( "The API handler returned an error: %1" ).arg( ex.what() ) );
}
break;
}
}
// Throw
if ( ! hasMatch )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Requested URI does not match any registered API handler" ) );
}
}
const QMap<QgsServerOgcApi::ContentType, QString> QgsServerOgcApi::contentTypeMimes()
{
return sContentTypeMime;
}
const QHash<QgsServerOgcApi::ContentType, QList<QgsServerOgcApi::ContentType> > QgsServerOgcApi::contentTypeAliases()
{
return sContentTypeAliases;
}
std::string QgsServerOgcApi::relToString( const Rel &rel )
{
static QMetaEnum metaEnum = QMetaEnum::fromType<QgsServerOgcApi::Rel>();
return metaEnum.valueToKey( rel );
}
QString QgsServerOgcApi::contentTypeToString( const ContentType &ct )
{
static QMetaEnum metaEnum = QMetaEnum::fromType<ContentType>();
QString result { metaEnum.valueToKey( ct ) };
return result.replace( '_', '-' );
}
std::string QgsServerOgcApi::contentTypeToStdString( const ContentType &ct )
{
static QMetaEnum metaEnum = QMetaEnum::fromType<ContentType>();
return metaEnum.valueToKey( ct );
}
QString QgsServerOgcApi::contentTypeToExtension( const ContentType &ct )
{
return contentTypeToString( ct ).toLower();
}
QgsServerOgcApi::ContentType QgsServerOgcApi::contenTypeFromExtension( const std::string &extension )
{
return sContentTypeMime.key( QString::fromStdString( extension ) );
}
std::string QgsServerOgcApi::mimeType( const QgsServerOgcApi::ContentType &contentType )
{
return sContentTypeMime.value( contentType, QString() ).toStdString();
}
const std::vector<std::shared_ptr<QgsServerOgcApiHandler> > QgsServerOgcApi::handlers() const
{
return mHandlers;
}

View File

@ -0,0 +1,195 @@
/***************************************************************************
qgsserverogcapi.h - QgsServerOgcApi
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 QGSSERVEROGCAPI_H
#define QGSSERVEROGCAPI_H
#include "qgsserverapi.h"
#include "qgis_server.h"
class QgsServerOgcApiHandler;
/**
* \ingroup server
* QGIS Server OGC API endpoint. QgsServerOgcApi provides the foundation for
* the new generation of REST-API based OGC services (e.g. WFS3).
*
* This class can be used directly and configured by registering handlers
* as instances of QgsServerOgcApiHandler.
*
* \code{.py}
*
* \endcode
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerOgcApi : public QgsServerApi
{
Q_GADGET
public:
// Note: non a scoped enum or qHash fails
//! Rel link types
enum Rel
{
// The following registered link relation types are used
alternate, //! Refers to a substitute for this context.
describedBy, //! Refers to a resource providing information about the links context.
collection, //! The target IRI points to a resource that is a member of the collection represented by the context IRI.
item, //! The target IRI points to a resource that is a member of the collection represented by the context IRI.
self, //! Conveys an identifier for the links context.
service_desc, //! Identifies service description for the context that is primarily intended for consumption by machines.
service_doc, //! Identifies service documentation for the context that is primarily intended for human consumption.
prev, //! Indicates that the links context is a part of a series, and that the previous in the series is the link targe
next, //! Indicates that the links context is a part of a series, and that the next in the series is the link target.
license, //! Refers to a license associated with this context.
// In addition the following link relation types are used for which no applicable registered link relation type could be identified:
items, //! Refers to a resource that is comprised of members of the collection represented by the links context.
conformance, //! The target IRI points to a resource which represents the collection resource for the context IRI.
data //! The target IRI points to resource data
};
Q_ENUM( Rel )
// Note: cannot be a scoped enum because qHash does not support them
//! Media types used for content negotiation, insert more specific first
enum ContentType
{
GEOJSON,
OPENAPI3, //! "application/openapi+json;version=3.0"
JSON,
HTML
};
Q_ENUM( ContentType )
/**
* QgsServerOgcApi constructor
* \param serverIface pointer to the server interface
* \param rootPath root path for this API (usually starts with a "/", e.g. "/wfs3")
* \param name API name
* \param description API description
* \param version API version
*/
QgsServerOgcApi( QgsServerInterface *serverIface,
const QString &rootPath,
const QString &name,
const QString &description = QString(),
const QString &version = QString() );
// QgsServerApi interface
const QString name() const override { return mName; }
const QString description() const override { return mDescription; }
const QString version() const override { return mVersion; }
const QString rootPath() const override { return mRootPath ; }
~QgsServerOgcApi() override;
/**
* Executes a request by passing the given \a context to the API handlers.
*/
virtual void executeRequest( const QgsServerApiContext &context ) const override SIP_THROW( QgsServerApiBadRequestException ) SIP_VIRTUALERRORHANDLER( serverapi_badrequest_exception_handler );
/**
* Returns a map of contentType => mime type
* \note not available in Python bindings
*/
static const QMap<QgsServerOgcApi::ContentType, QString> contentTypeMimes() SIP_SKIP;
/**
* Returns contenType specializations (e.g. JSON => [GEOJSON, OPENAPI3], XML => [GML])
* \note not available in Python bindings
*/
static const QHash<QgsServerOgcApi::ContentType, QList<QgsServerOgcApi::ContentType> > contentTypeAliases() SIP_SKIP;
// Utilities
#ifndef SIP_RUN
/**
* Registers an OGC API handler passing \a Args to the constructor
* \note not available in Python bindings
*/
template<class T, typename... Args>
void registerHandler( Args... args )
{
mHandlers.emplace_back( std::make_shared<T>( args... ) );
}
#endif
/**
* Registers an OGC API \a handler, ownership of the handler is transferred to the API
*/
void registerHandler( QgsServerOgcApiHandler *handler SIP_TRANSFER );
/**
* Returns a sanitized \a url with extra slashes removed
*/
static QUrl sanitizeUrl( const QUrl &url );
/**
* Returns the string representation of \a rel attribute.
*/
static std::string relToString( const QgsServerOgcApi::Rel &rel );
/**
* Returns the string representation of a \a ct (Content-Type) attribute.
*/
static QString contentTypeToString( const QgsServerOgcApi::ContentType &ct );
/**
* Returns the string representation of a \a ct (Content-Type) attribute.
*/
static std::string contentTypeToStdString( const QgsServerOgcApi::ContentType &ct );
/**
* Returns the file extension for a \a ct (Content-Type).
*/
static QString contentTypeToExtension( const QgsServerOgcApi::ContentType &ct );
/**
* Returns the Content-Type value corresponding to \a extension.
*/
static QgsServerOgcApi::ContentType contenTypeFromExtension( const std::string &extension );
/**
* Returns the mime-type for the \a contentType or an empty string if not found
*/
static std::string mimeType( const QgsServerOgcApi::ContentType &contentType );
/**
* Returns registered handlers
*/
const std::vector<std::shared_ptr<QgsServerOgcApiHandler> > handlers() const SIP_SKIP;
private:
QString mRootPath;
QString mName;
QString mDescription;
QString mVersion;
//Note: this cannot be unique because of SIP bindings
std::vector<std::shared_ptr<QgsServerOgcApiHandler>> mHandlers;
//! Stores content type mime strings
static QMap<QgsServerOgcApi::ContentType, QString> sContentTypeMime;
//! Stores content type aliases (e.g. JSON->[GEOJSON,OPENAPI3], XML->[GML] )
static QHash<QgsServerOgcApi::ContentType, QList<QgsServerOgcApi::ContentType>> sContentTypeAliases;
};
#endif // QGSSERVEROGCAPI_H

View File

@ -0,0 +1,494 @@
/***************************************************************************
qgsserverogcapihandler.cpp - QgsServerOgcApiHandler
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 <QDateTime>
#include <QFileInfo>
#include <QDir>
#include <QDebug>
#include "qgsmessagelog.h"
#include "qgsproject.h"
#include "qgsjsonutils.h"
#include "qgsvectorlayer.h"
#include "qgsserverogcapihandler.h"
#include "qgsserverapiutils.h"
#include "qgsserverresponse.h"
#include "qgsserverinterface.h"
#include "nlohmann/json.hpp"
#include "inja/inja.hpp"
using json = nlohmann::json;
using namespace inja;
QVariantMap QgsServerOgcApiHandler::values( const QgsServerApiContext &context ) const
{
QVariantMap result ;
QVariantList positional;
const auto constParameters { parameters( context ) };
for ( const auto &p : constParameters )
{
// value() calls the validators and throw an exception if validation fails
result[p.name()] = p.value( context );
}
const auto match { path().match( context.request()->url().toString() ) };
if ( match.hasMatch() )
{
const auto constNamed { path().namedCaptureGroups() };
// Get named path parameters
for ( const auto &name : constNamed )
{
if ( ! name.isEmpty() )
result[name] = QUrlQuery( match.captured( name ) ).toString() ;
}
}
return result;
}
QgsServerOgcApiHandler::~QgsServerOgcApiHandler()
{
//qDebug() << "handler destroyed";
}
QString QgsServerOgcApiHandler::contentTypeForAccept( const QString &accept ) const
{
QString result;
const auto constMimes { QgsServerOgcApi::contentTypeMimes() };
for ( const auto &ct : constMimes )
{
if ( accept.contains( ct ) )
{
result = ct;
break;
}
}
return result;
}
void QgsServerOgcApiHandler::write( json &data, const QgsServerApiContext &context, const json &htmlMetadata ) const
{
const auto contentType { contentTypeFromRequest( context.request() ) };
switch ( contentType )
{
case QgsServerOgcApi::ContentType::HTML:
data["handler"] = schema( context );
if ( ! htmlMetadata.is_null() )
{
data["metadata"] = htmlMetadata;
}
htmlDump( data, context );
break;
case QgsServerOgcApi::ContentType::GEOJSON:
case QgsServerOgcApi::ContentType::JSON:
case QgsServerOgcApi::ContentType::OPENAPI3:
jsonDump( data, context, QgsServerOgcApi::contentTypeMimes().value( contentType ) );
break;
}
}
void QgsServerOgcApiHandler::write( QVariant &data, const QgsServerApiContext &context, const QVariantMap &htmlMetadata ) const
{
json j { QgsJsonUtils::jsonFromVariant( data ) };
json jm { QgsJsonUtils::jsonFromVariant( htmlMetadata ) };
QgsServerOgcApiHandler::write( j, context, jm );
}
std::string QgsServerOgcApiHandler::href( const QgsServerApiContext &context, const QString &extraPath, const QString &extension ) const
{
QUrl url { context.request()->url() };
QString urlBasePath { context.matchedPath() };
const auto match { path().match( url.path() ) };
if ( match.captured().count() > 0 )
{
url.setPath( urlBasePath + match.captured( 0 ) );
}
else
{
url.setPath( urlBasePath );
}
// Remove any existing extension
const auto suffixLength { QFileInfo( url.path() ).completeSuffix().length() };
if ( suffixLength > 0 )
{
auto path {url.path()};
path.truncate( path.length() - ( suffixLength + 1 ) );
url.setPath( path );
}
// Add extra path
url.setPath( url.path() + extraPath );
// (re-)add extension
// JSON is the default anyway, we don'n need to add it
if ( ! extension.isEmpty() )
{
// Remove trailing slashes if any.
QString path { url.path() };
while ( path.endsWith( '/' ) )
{
path.chop( 1 );
}
url.setPath( path + '.' + extension );
}
return QgsServerOgcApi::sanitizeUrl( url ).toString( QUrl::FullyEncoded ).toStdString();
}
void QgsServerOgcApiHandler::jsonDump( json &data, const QgsServerApiContext &context, const QString &contentType ) const
{
QDateTime time { QDateTime::currentDateTime() };
time.setTimeSpec( Qt::TimeSpec::UTC );
data["timeStamp"] = time.toString( Qt::DateFormat::ISODate ).toStdString() ;
context.response()->setHeader( QStringLiteral( "Content-Type" ), contentType );
#ifdef QGISDEBUG
context.response()->write( data.dump( 2 ) );
#else
context.response()->write( data.dump( ) );
#endif
}
json QgsServerOgcApiHandler::schema( const QgsServerApiContext &context ) const
{
Q_UNUSED( context );
return nullptr;
}
json QgsServerOgcApiHandler::link( const QgsServerApiContext &context, const QgsServerOgcApi::Rel &linkType, const QgsServerOgcApi::ContentType contentType, const std::string &title ) const
{
json l
{
{
"href", href( context, "/",
QgsServerOgcApi::contentTypeToExtension( contentType ) )
},
{ "rel", QgsServerOgcApi::relToString( linkType ) },
{ "type", QgsServerOgcApi::mimeType( contentType ) },
{ "title", title != "" ? title : linkTitle() },
};
return l;
}
json QgsServerOgcApiHandler::links( const QgsServerApiContext &context ) const
{
const QgsServerOgcApi::ContentType currentCt { contentTypeFromRequest( context.request() ) };
json links = json::array();
const QList<QgsServerOgcApi::ContentType> constCts { contentTypes() };
for ( const auto &ct : constCts )
{
links.push_back( link( context, ( ct == currentCt ? QgsServerOgcApi::Rel::self :
QgsServerOgcApi::Rel::alternate ), ct,
linkTitle() + " as " + QgsServerOgcApi::contentTypeToStdString( ct ) ) );
}
return links;
}
QgsVectorLayer *QgsServerOgcApiHandler::layerFromContext( const QgsServerApiContext &context ) const
{
if ( ! context.project() )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Project is invalid or undefined" ) );
}
// Check collectionId
const QRegularExpressionMatch match { path().match( context.request()->url().path( ) ) };
if ( ! match.hasMatch() )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Collection was not found" ) );
}
const QString collectionId { match.captured( QStringLiteral( "collectionId" ) ) };
// May throw if not found
return layerFromCollection( context, collectionId );
}
const QString QgsServerOgcApiHandler::staticPath( const QgsServerApiContext &context ) const
{
// resources/server/api + /static
return context.serverInterface()->serverSettings()->apiResourcesDirectory() + QStringLiteral( "/ogc/static" );
}
const QString QgsServerOgcApiHandler::templatePath( const QgsServerApiContext &context ) const
{
// resources/server/api + /ogc/templates/ + operationId + .html
QString path { context.serverInterface()->serverSettings()->apiResourcesDirectory() };
path += QStringLiteral( "/ogc/templates" );
path += context.apiRootPath();
path += '/';
path += QString::fromStdString( operationId() );
path += QStringLiteral( ".html" );
return path;
}
void QgsServerOgcApiHandler::htmlDump( const json &data, const QgsServerApiContext &context ) const
{
context.response()->setHeader( QStringLiteral( "Content-Type" ), QStringLiteral( "text/html" ) );
auto path { templatePath( context ) };
if ( ! QFile::exists( path ) )
{
QgsMessageLog::logMessage( QStringLiteral( "Template not found error: %1" ).arg( path ), QStringLiteral( "Server" ), Qgis::Critical );
throw QgsServerApiBadRequestException( QStringLiteral( "Template not found: %1" ).arg( QFileInfo( path ).fileName() ) );
}
QFile f( path );
if ( ! f.open( QFile::ReadOnly | QFile::Text ) )
{
QgsMessageLog::logMessage( QStringLiteral( "Could not open template file: %1" ).arg( path ), QStringLiteral( "Server" ), Qgis::Critical );
throw QgsServerApiInternalServerError( QStringLiteral( "Could not open template file: %1" ).arg( QFileInfo( path ).fileName() ) );
}
try
{
// Get the template directory and the file name
QFileInfo pathInfo { path };
Environment env { ( pathInfo.dir().path() + QDir::separator() ).toStdString() };
// For template debugging:
env.add_callback( "json_dump", 0, [ = ]( Arguments & )
{
return data.dump();
} );
// Path manipulation: appends a directory path to the current url
env.add_callback( "path_append", 1, [ = ]( Arguments & args )
{
auto url { context.request()->url() };
QFileInfo fi{ url.path() };
auto suffix { fi.suffix() };
auto fName { fi.filePath()};
fName.chop( suffix.length() + 1 );
fName += '/' + QString::number( args.at( 0 )->get<QgsFeatureId>( ) );
if ( !suffix.isEmpty() )
{
fName += '.' + suffix;
}
fi.setFile( fName );
url.setPath( fi.filePath() );
return url.toString().toStdString();
} );
// Path manipulation: removes the specified number of directory components from the current url path
env.add_callback( "path_chomp", 1, [ = ]( Arguments & args )
{
QUrl url { QString::fromStdString( args.at( 0 )->get<std::string>( ) ) };
QFileInfo fi{ url.path() };
auto suffix { fi.suffix() };
auto fName { fi.filePath()};
fName.chop( suffix.length() + 1 );
// Chomp last segment
fName = fName.replace( QRegularExpression( R"raw(\/[^/]+$)raw" ), QString() );
if ( !suffix.isEmpty() )
{
fName += '.' + suffix;
}
fi.setFile( fName );
url.setPath( fi.filePath() );
return url.toString().toStdString();
} );
// Returns filtered links from a link list
// links_filter( <links>, <key>, <value> )
env.add_callback( "links_filter", 3, [ = ]( Arguments & args )
{
json links { args.at( 0 )->get<json>( ) };
std::string key { args.at( 1 )->get<std::string>( ) };
std::string value { args.at( 2 )->get<std::string>( ) };
json result = json::array();
for ( const auto &l : links )
{
if ( l[key] == value )
{
result.push_back( l );
}
}
return result;
} );
// Returns a short name from content types
env.add_callback( "content_type_name", 1, [ = ]( Arguments & args )
{
const QgsServerOgcApi::ContentType ct { QgsServerOgcApi::contenTypeFromExtension( args.at( 0 )->get<std::string>( ) ) };
return QgsServerOgcApi::contentTypeToStdString( ct );
} );
// Static: returns the full URL to the specified static <path>
env.add_callback( "static", 1, [ = ]( Arguments & args )
{
auto asset( args.at( 0 )->get<std::string>( ) );
return context.matchedPath().toStdString() + "/static/" + asset;
} );
context.response()->write( env.render_file( pathInfo.fileName().toStdString(), data ) );
}
catch ( std::exception &e )
{
QgsMessageLog::logMessage( QStringLiteral( "Error parsing template file: %1 - %2" ).arg( path, e.what() ), QStringLiteral( "Server" ), Qgis::Critical );
throw QgsServerApiInternalServerError( QStringLiteral( "Error parsing template file: %1" ).arg( e.what() ) );
}
}
QgsServerOgcApi::ContentType QgsServerOgcApiHandler::contentTypeFromRequest( const QgsServerRequest *request ) const
{
// Fallback to default
QgsServerOgcApi::ContentType result { defaultContentType() };
bool found { false };
// First file extension ...
const QString extension { QFileInfo( request->url().path() ).completeSuffix().toUpper() };
if ( ! extension.isEmpty() )
{
static QMetaEnum metaEnum { QMetaEnum::fromType<QgsServerOgcApi::ContentType>() };
bool ok { false };
const int ct { metaEnum.keyToValue( extension.toLocal8Bit().constData(), &ok ) };
if ( ok )
{
result = static_cast<QgsServerOgcApi::ContentType>( ct );
found = true;
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "The client requested an unsupported extension: %1" ).arg( extension ), QStringLiteral( "Server" ), Qgis::Warning );
}
}
// ... then "Accept"
const QString accept { request->header( QStringLiteral( "Accept" ) ) };
if ( ! found && ! accept.isEmpty() )
{
const QString ctFromAccept { contentTypeForAccept( accept ) };
if ( ! ctFromAccept.isEmpty() )
{
result = QgsServerOgcApi::contentTypeMimes().key( ctFromAccept );
found = true;
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "The client requested an unsupported content type in Accept header: %1" ).arg( accept ), QStringLiteral( "Server" ), Qgis::Warning );
}
}
// Validation: check if the requested content type (or an alias) is supported by the handler
if ( ! contentTypes().contains( result ) )
{
// Check aliases
bool found { false };
if ( QgsServerOgcApi::contentTypeAliases().keys().contains( result ) )
{
const QList<QgsServerOgcApi::ContentType> constCt { contentTypes() };
for ( const auto &ct : constCt )
{
if ( QgsServerOgcApi::contentTypeAliases()[result].contains( ct ) )
{
result = ct;
found = true;
break;
}
}
}
if ( ! found )
{
QgsMessageLog::logMessage( QStringLiteral( "Unsupported Content-Type: %1" ).arg( QgsServerOgcApi::contentTypeToString( result ) ), QStringLiteral( "Server" ), Qgis::Info );
throw QgsServerApiBadRequestException( QStringLiteral( "Unsupported Content-Type: %1" ).arg( QgsServerOgcApi::contentTypeToString( result ) ) );
}
}
return result;
}
QString QgsServerOgcApiHandler::parentLink( const QUrl &url, int levels )
{
QString path { url.path() };
const QFileInfo fi { path };
const QString suffix { fi.suffix() };
if ( ! suffix.isEmpty() )
{
path.chop( suffix.length() + 1 );
}
while ( path.endsWith( '/' ) )
{
path.chop( 1 );
}
QRegularExpression re( R"raw(\/[^/]+$)raw" );
for ( int i = 0; i < levels ; i++ )
{
path = path.replace( re, QString() );
}
QUrl result( url );
QList<QPair<QString, QString> > qi;
const auto constItems { result.queryItems( ) };
for ( const auto &i : constItems )
{
if ( i.first.compare( QStringLiteral( "MAP" ), Qt::CaseSensitivity::CaseInsensitive ) == 0 )
{
qi.push_back( i );
}
}
result.setQueryItems( qi );
result.setPath( path );
return result.toString();
}
QgsVectorLayer *QgsServerOgcApiHandler::layerFromCollection( const QgsServerApiContext &context, const QString &collectionId )
{
const auto mapLayers { context.project()->mapLayersByShortName<QgsVectorLayer *>( collectionId ) };
if ( mapLayers.count() != 1 )
{
throw QgsServerApiImproperlyConfiguredException( QStringLiteral( "Collection with given id (%1) was not found or multiple matches were found" ).arg( collectionId ) );
}
return mapLayers.first();
}
json QgsServerOgcApiHandler::defaultResponse()
{
static json defRes =
{
{
"default", {
{ "description", "An error occurred." },
{
"content", {
{
"application/json", {
{
"schema", {
{ "$ref", "#/components/schemas/exception" }
}
},
{
"text/html", {
{
"schema", {
{ "type", "string" }
}
}
}
}
}
}
}
}
}
}
};
return defRes;
}
json QgsServerOgcApiHandler::jsonTags() const
{
return QgsJsonUtils::jsonFromVariant( tags() );
}

View File

@ -0,0 +1,337 @@
/***************************************************************************
qgsserverogcapihandler.h - QgsServerOgcApiHandler
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 QGSSERVEROGCAPIHANDLER_H
#define QGSSERVEROGCAPIHANDLER_H
#include <QRegularExpression>
#include "qgis_server.h"
#include "qgsserverquerystringparameter.h"
#include "qgsserverogcapi.h"
#include "nlohmann/json_fwd.hpp"
#include "inja/inja.hpp"
#ifndef SIP_RUN
using json = nlohmann::json;
#endif
class QgsServerApiContext;
/**
* \ingroup server
* The QgsServerOgcApiHandler abstract class represents a OGC API handler to be registered
* in QgsServerOgcApi class.
*
* Subclasses must override operational and informative methods and define
* the core functionality in handleRequest() method.
*
* The following methods MUST be implemented:
* - path
* - operationId
* - summary (shorter text)
* - description (longer text)
* - linkTitle
* - linkType
* - schema
*
* Optionally, override:
* - tags
* - parameters
* - contentTypes
* - defaultContentType
*
*
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerOgcApiHandler
{
public:
virtual ~QgsServerOgcApiHandler();
// /////////////////////////////////////////////
// MAIN Section (operational)
/**
* URL pattern for this handler, named capture group are automatically
* extracted and returned by values()
*
* Example: "/handlername/(?P<code1>\d{2})/items" will capture "code1" as a
* named parameter.
*
* \see values()
*/
virtual QRegularExpression path() const = 0;
//! Returns the operation id for template file names and other internal references
virtual std::string operationId() const = 0;
/**
* Returns a list of query string parameters.
*
* Depending on the handler, it may be dynamic (per-request) or static.
* \param context the request context
*/
virtual QList<QgsServerQueryStringParameter> parameters( const QgsServerApiContext &context ) const { Q_UNUSED( context ); return { }; }
// /////////////////////////////////////////////
// METADATA Sections (informative)
//! Summary
virtual std::string summary() const = 0;
//! Description
virtual std::string description() const = 0;
//! Title for the handler link
virtual std::string linkTitle() const = 0;
//! Main role for the resource link
virtual QgsServerOgcApi::Rel linkType() const = 0;
//! Tags
virtual QStringList tags() const { return {}; }
/**
* Returns the default response content type in case the client did not specifically
* ask for any particular content type.
*/
virtual QgsServerOgcApi::ContentType defaultContentType() const { return QgsServerOgcApi::ContentType::JSON; }
/**
* Returns the list of content types this handler can serve, default to JSON and HTML.
* In case a specialized type (such as GEOJSON) is supported,
* the generic type (such as JSON) should not be listed.
*/
virtual QList<QgsServerOgcApi::ContentType> contentTypes() const { return { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML }; }
/**
* Handles the request within its \a context
*
* Subclasses must implement this methods, and call validate() to
* extract validated parameters from the request.
*
* \throws QgsServerApiBadRequestError if the method encounters any error
*/
virtual void handleRequest( const QgsServerApiContext &context ) const = 0;
/**
* Analyzes the incoming request \a context and returns the validated
* parameter map, throws QgsServerApiBadRequestError in case of errors.
*
* Path fragments from the named groups in the path() regular expression
* are also added to the map.
*
* Your handleRequest method should call this function to retrieve
* the parameters map.
*
* \returns the validated parameters map by extracting captured
* named parameters from the path (no validation is performed on
* the type because the regular expression can do it),
* and the query string parameters.
*
* \see path()
* \see parameters()
* \throws QgsServerApiBadRequestError if validation fails
*/
virtual QVariantMap values( const QgsServerApiContext &context ) const SIP_THROW( QgsServerApiBadRequestException );
/**
* Looks for the first ContentType match in the accept header and returns its mime type,
* returns an empty string if there are not matches.
*/
QString contentTypeForAccept( const QString &accept ) const;
// /////////////////////////////////////////////////////
// Utility methods: override should not be required
#ifndef SIP_RUN // Skip SIP
/**
* Writes \a data to the \a context response stream, content-type is calculated from the \a context request,
* optional \a htmlMetadata for the HTML templates can be specified and will be added as "metadata" to
* the HTML template variables.
*
* HTML output uses a template engine.
*
* Available template functions:
* See: https://github.com/pantor/inja#tutorial
*
* Available custom template functions:
* - path_append( path ): appends a directory path to the current url
* - path_chomp( n ):removes the specified number "n" of directory components from the current url path
* - json_dump( ): prints current JSON data passed to the template
* - static( path ): returns the full URL to the specified static path, for example:
* static( "/style/black.css" ) will return something like "/wfs3/static/style/black.css".
* - links_filter( links, key, value ): Returns filtered links from a link list
* - content_type_name( content_type ): Returns a short name from a content type for example "text/html" will return "HTML"
*
* \note not available in Python bindings
*/
void write( json &data, const QgsServerApiContext &context, const json &htmlMetadata = nullptr ) const;
/**
* Writes \a data to the \a context response stream as JSON
* (indented if debug is active), an optional \a contentType can be specified.
*
* \note not available in Python bindings
*/
void jsonDump( json &data, const QgsServerApiContext &context, const QString &contentType = QStringLiteral( "application/json" ) ) const;
/**
* Writes \a data as HTML to the response stream in \a context using a template.
*
* \see templatePath()
* \note not available in Python bindings
*/
void htmlDump( const json &data, const QgsServerApiContext &context ) const;
/**
* Returns handler information from the \a context for the OPENAPI description (id, description and other metadata) as JSON.
* It may return a NULL JSON object in case the handler does not need to be included in the API.
*
* \note requires a valid project to be present in the context
* \note not available in Python bindings
*/
virtual json schema( const QgsServerApiContext &context ) const;
/**
* Builds and returns a link to the resource.
*
* \param context request context
* \param linkType type of the link (rel attribute), default to SELF
* \param contentType content type of the link (default to JSON)
* \param title title of the link
* \note not available in Python bindings
*/
json link( const QgsServerApiContext &context,
const QgsServerOgcApi::Rel &linkType = QgsServerOgcApi::Rel::self,
const QgsServerOgcApi::ContentType contentType = QgsServerOgcApi::ContentType::JSON,
const std::string &title = "" ) const;
/**
* Returns all the links for the given request \a context.
*
* The base implementation returns the alternate and self links, subclasses may
* add other links.
*
* \note not available in Python bindings
*/
json links( const QgsServerApiContext &context ) const;
/**
* Returns a vector layer instance from the "collectionId" parameter of the path in the given \a context,
* requires a valid project instance in the context.
*
* \note not available in Python bindings
*
* \throws QgsServerApiNotFoundError if the layer could not be found
* \throws QgsServerApiImproperlyConfiguredException if project is not set
*/
QgsVectorLayer *layerFromContext( const QgsServerApiContext &context ) const;
#endif // SIP skipped
/**
* Writes \a data to the \a context response stream, content-type is calculated from the \a context request,
* optional \a htmlMetadata for the HTML templates can be specified and will be added as "metadata" to
* the HTML template variables.
*
* HTML output uses a template engine.
*
* Available template functions:
* See: https://github.com/pantor/inja#tutorial
*
* Available custom template functions:
* - path_append( path ): appends a directory path to the current url
* - path_chomp( n ): removes the specified number "n" of directory components from the current url path
* - json_dump(): prints current JSON data passed to the template
* - static( path): returns the full URL to the specified static path, for example:
* static("/style/black.css") will return something like "/wfs3/static/style/black.css".
* - links_filter( links, key, value ): returns filtered links from a link list
* - content_type_name( content_type ): returns a short name from a content type for example "text/html" will return "HTML"
*
*/
void write( QVariant &data, const QgsServerApiContext &context, const QVariantMap &htmlMetadata = QVariantMap() ) const;
/**
* Returns an URL to self, to be used for links to the current resources and as a base for constructing links to sub-resources
*
* \param context the current request context
* \param extraPath an optional extra path that will be appended to the calculated URL
* \param extension optional file extension to add (the dot will be added automatically).
*/
std::string href( const QgsServerApiContext &context, const QString &extraPath = QString(), const QString &extension = QString() ) const;
/**
* Returns the HTML template path for the handler in the given \a context
*
* The template path is calculated from QgsServerSettings's apiResourcesDirectory() as follow:
* apiResourcesDirectory() + "/ogc/templates/" + context.apiRootPath + operationId + ".html"
* e.g. for an API with root path "/wfs3" and an handler with operationId "collectionItems", the path
* will be apiResourcesDirectory() + "/ogc/templates/wfs3/collectionItems.html"
*/
const QString templatePath( const QgsServerApiContext &context ) const;
/**
* Returns the absolute path to the base directory where static resources for
* this handler are stored in the given \a context.
*
*/
const QString staticPath( const QgsServerApiContext &context ) const;
/**
* Returns the content type from the \a request.
*
* The path file extension is examined first and checked for known mime types,
* the "Accept" HTTP header is examined next.
* Fallback to the default content type of the handler if none of the above matches.
*
* \throws QgsServerApiBadRequestError if the content type of the request is not compatible with the handler (\see contentTypes member)
*/
QgsServerOgcApi::ContentType contentTypeFromRequest( const QgsServerRequest *request ) const;
/**
* Returns a link to the parent page up to \a levels in the HTML hierarchy from the given \a url, MAP query argument is preserved
*/
static QString parentLink( const QUrl &url, int levels = 1 );
/**
* Returns a vector layer from the \a collectionId in the given \a context
*/
static QgsVectorLayer *layerFromCollection( const QgsServerApiContext &context, const QString &collectionId );
/**
* Returns the defaultResponse as JSON
*
* \note not available in Python bindings
*/
static json defaultResponse() SIP_SKIP;
/**
* Returns tags as JSON
*
* \see tags()
*
* \note not available in Python bindings
*/
json jsonTags( ) const SIP_SKIP;
};
#endif // QGSSERVEROGCAPIHANDLER_H

View File

@ -0,0 +1,158 @@
/***************************************************************************
qgsserverquerystringparameter.cpp - QgsServerQueryStringParameter
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 "qgsserverquerystringparameter.h"
#include "qgsserverrequest.h"
#include "qgsserverexception.h"
#include "nlohmann/json.hpp"
QgsServerQueryStringParameter::QgsServerQueryStringParameter( const QString name,
bool required,
QgsServerQueryStringParameter::Type type,
const QString &description,
const QVariant &defaultValue ):
mName( name ),
mRequired( required ),
mType( type ),
mDescription( description ),
mDefaultValue( defaultValue )
{
}
QgsServerQueryStringParameter::~QgsServerQueryStringParameter()
{
}
QVariant QgsServerQueryStringParameter::value( const QgsServerApiContext &context ) const
{
// 1: check required
if ( mRequired && ! context.request()->url().hasQueryItem( mName ) )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Missing required argument: '%1'" ).arg( mName ) );
}
// 2: get value from query string or set it to the default
QVariant value;
if ( context.request()->url().hasQueryItem( mName ) )
{
value = QUrlQuery( context.request()->url() ).queryItemValue( mName, QUrl::FullyDecoded );
}
else if ( mDefaultValue.isValid() )
{
value = mDefaultValue;
}
if ( value.isValid() )
{
// 3: check type
const QVariant::Type targetType { static_cast< QVariant::Type >( mType )};
// Handle csv list type
if ( mType == Type::List )
{
value = value.toString().split( ',' );
}
if ( value.type() != targetType )
{
bool ok = false;
if ( value.canConvert( static_cast<int>( targetType ) ) )
{
ok = true;
switch ( mType )
{
case Type::String:
value = value.toString( );
break;
case Type::Boolean:
value = value.toBool( );
break;
case Type::Double:
value = value.toDouble( &ok );
break;
case Type::Integer:
value = value.toLongLong( &ok );
break;
case Type::List:
// already converted to a string list
break;
}
}
if ( ! ok )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Argument '%1' could not be converted to %2" ).arg( mName )
.arg( typeName( mType ) ) );
}
}
// 4: check custom validation
if ( mCustomValidator && ! mCustomValidator( context, value ) )
{
throw QgsServerApiBadRequestException( QStringLiteral( "Argument '%1' is not valid. %2" ).arg( name() ).arg( description() ) );
}
}
return value;
}
void QgsServerQueryStringParameter::setCustomValidator( const customValidator &customValidator )
{
mCustomValidator = customValidator;
}
json QgsServerQueryStringParameter::data() const
{
const auto nameString { name().toStdString() };
auto dataType { typeName( mType ).toLower().toStdString() };
// Map list to string because it will be serialized
if ( dataType == "list" )
{
dataType = "string";
}
return
{
{ "name", nameString },
{ "description", "Filter the collection by '" + nameString + "'" },
{ "required", mRequired },
{ "in", "query"},
{ "style", "form"},
{ "explode", false },
{ "schema", {{ "type", dataType }}},
// This is unfortunately not in OAS: { "default", mDefaultValue.toString().toStdString() }
};
}
QString QgsServerQueryStringParameter::description() const
{
return mDescription;
}
QString QgsServerQueryStringParameter::typeName( const QgsServerQueryStringParameter::Type type )
{
static QMetaEnum metaEnum = QMetaEnum::fromType<Type>();
return metaEnum.valueToKey( static_cast<int>( type ) );
}
QString QgsServerQueryStringParameter::name() const
{
return mName;
}
void QgsServerQueryStringParameter::setDescription( const QString &description )
{
mDescription = description;
}

View File

@ -0,0 +1,155 @@
/***************************************************************************
qgsserverquerystringparameter.h - QgsServerQueryStringParameter
---------------------
begin : 10.7.2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************
* *
* 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 QGSSERVERQUERYSTRINGPARAMETER_H
#define QGSSERVERQUERYSTRINGPARAMETER_H
#include "qgsserverapicontext.h"
#include "qgis_server.h"
#include "qgis_sip.h"
#include <QString>
#include <QVariant>
#include <QObject>
#include "nlohmann/json_fwd.hpp"
#ifndef SIP_RUN
using json = nlohmann::json;
#endif
class QgsServerApiBadRequestException;
/**
* The QgsServerQueryStringParameter class holds the information regarding
* a query string input parameter and its validation.
*
* The class is extendable through custom validators (C++ only) and/or by
* subclassing and overriding the value() method.
*
* \ingroup server
* \since QGIS 3.10
*/
class SERVER_EXPORT QgsServerQueryStringParameter
{
Q_GADGET
#ifndef SIP_RUN
typedef std::function< bool ( const QgsServerApiContext &, QVariant & ) > customValidator;
#endif
public:
/**
* The Type enum represents the parameter type
*/
enum class Type
{
String = QVariant::String, //! parameter is a string
Integer = QVariant::LongLong, //! parameter is an integer
Double = QVariant::Double, //! parameter is a double
Boolean = QVariant::Bool, //! parameter is a boolean
List = QVariant::StringList, //! parameter is a (comma separated) list of strings, the handler will perform any further required conversion of the list values
};
Q_ENUM( Type )
/**
* Constructs a QgsServerQueryStringParameter object.
*
* \param name parameter name
* \param required
* \param type the parameter type
* \param description parameter description
* \param defaultValue default value, it is ignored if the parameter is required
*/
QgsServerQueryStringParameter( const QString name,
bool required = false,
Type type = QgsServerQueryStringParameter::Type::String,
const QString &description = QString(),
const QVariant &defaultValue = QVariant() );
virtual ~QgsServerQueryStringParameter();
/**
* Extracts the value from the request \a context by validating the parameter
* value and converting it to its proper Type.
* If the value is not set and a default was not provided an invalid QVariant is returned.
*
* Validation steps:
* - required
* - can convert to proper Type
* - custom validator (if set - not available in Python bindings)
*
* \see setCustomValidator() (not available in Python bindings)
* \returns the parameter value or an invalid QVariant if not found (and not required)
* \throws QgsServerApiBadRequestError if validation fails
*/
virtual QVariant value( const QgsServerApiContext &context ) const;
#ifndef SIP_RUN
/**
* Sets the custom validation function to \a customValidator.
* Validator function signature is:
* bool ( const QgsServerApiContext &context, QVariant &value )
* \note a validator can change the value if needed and must return TRUE if the validation passed
* \note not available in Python bindings
*/
void setCustomValidator( const customValidator &customValidator );
/**
* Returns the handler information as a JSON object.
*/
json data( ) const;
#endif
/**
* Returns parameter description
*/
QString description() const;
/**
* Returns the name of the \a type
*/
static QString typeName( const Type type );
/**
* Returns the name of the parameter
*/
QString name() const;
/**
* Sets validator \a description
*/
void setDescription( const QString &description );
private:
QString mName;
bool mRequired = false;
Type mType = Type::String;
customValidator mCustomValidator = nullptr;
QString mDescription;
QVariant mDefaultValue;
friend class TestQgsServerQueryStringParameter;
};
#endif // QGSSERVERQUERYSTRINGPARAMETER_H

View File

@ -97,9 +97,14 @@ void QgsServerRequest::setParameter( const QString &key, const QString &value )
mUrl.setQuery( mParams.urlQuery() );
}
QString QgsServerRequest::parameter( const QString &key ) const
QString QgsServerRequest::parameter( const QString &key, const QString &defaultValue ) const
{
return mParams.value( key );
const auto value { mParams.value( key ) };
if ( value.isEmpty() )
{
return defaultValue;
}
return value;
}
void QgsServerRequest::removeParameter( const QString &key )
@ -119,3 +124,13 @@ void QgsServerRequest::setMethod( Method method )
{
mMethod = method;
}
const QString QgsServerRequest::queryParameter( const QString &name, const QString &defaultValue ) const
{
if ( ! mUrl.hasQueryItem( name ) )
{
return defaultValue;
}
return QUrl::fromPercentEncoding( mUrl.queryItemValue( name ).toUtf8() );
}

View File

@ -113,7 +113,7 @@ class SERVER_EXPORT QgsServerRequest
/**
* Gets a parameter value
*/
QString parameter( const QString &key ) const;
QString parameter( const QString &key, const QString &defaultValue = QString() ) const;
/**
* Remove a parameter
@ -172,6 +172,12 @@ class SERVER_EXPORT QgsServerRequest
*/
void setMethod( QgsServerRequest::Method method );
/**
* Returns the query string parameter with the given \a name from the request URL, a \a defaultValue can be specified.
* \since QGIS 3.10
*/
const QString queryParameter( const QString &name, const QString &defaultValue = QString( ) ) const;
protected:
/**

View File

@ -65,6 +65,11 @@ qint64 QgsServerResponse::write( const char *data )
return 0;
}
qint64 QgsServerResponse::write( const std::string data )
{
return write( data.c_str() );
}
void QgsServerResponse::write( const QgsServerException &ex )
{
QString responseFormat;

View File

@ -137,6 +137,17 @@ class SERVER_EXPORT QgsServerResponse
*/
virtual qint64 write( const char *data ) SIP_SKIP;
/**
* Writes at most maxSize bytes of data
*
* This is a convenient method that will write directly
* to the underlying I/O device
* \returns the number of bytes written
* \note not available in Python bindings
* \since QGIS 3.10
*/
virtual qint64 write( std::string data ) SIP_SKIP;
/**
* Write server exception
*/

View File

@ -20,6 +20,7 @@
#include "qgsapplication.h"
#include <QSettings>
#include <QDir>
QgsServerSettings::QgsServerSettings()
{
@ -183,6 +184,30 @@ void QgsServerSettings::initSettings()
QVariant()
};
mSettings[ sMaxWidth.envVar ] = sMaxWidth;
// API templates and static override directory
const Setting sApiResourcesDirectory = { QgsServerSettingsEnv::QGIS_SERVER_API_RESOURCES_DIRECTORY,
QgsServerSettingsEnv::DEFAULT_VALUE,
QStringLiteral( "Base directory where HTML templates and static assets (e.g. images, js and css files) are searched for" ),
QStringLiteral( "/qgis/server_api_resources_directory" ),
QVariant::String,
QDir( QgsApplication::pkgDataPath() ).absoluteFilePath( QStringLiteral( "resources/server/api" ) ),
QString()
};
mSettings[ sApiResourcesDirectory.envVar ] = sApiResourcesDirectory;
// API WFS3 max limit
const Setting sApiWfs3MaxLimit = { QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_MAX_LIMIT,
QgsServerSettingsEnv::DEFAULT_VALUE,
QStringLiteral( "Maximum value for \"limit\" in a features request, defaults to 10000" ),
QStringLiteral( "/qgis/server_api_wfs3_max_limit" ),
QVariant::LongLong,
QVariant( 10000 ),
QVariant()
};
mSettings[ sApiWfs3MaxLimit.envVar ] = sApiWfs3MaxLimit;
}
void QgsServerSettings::load()
@ -383,3 +408,13 @@ int QgsServerSettings::wmsMaxWidth() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_WMS_MAX_WIDTH ).toInt();
}
QString QgsServerSettings::apiResourcesDirectory() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_API_RESOURCES_DIRECTORY ).toString();
}
qlonglong QgsServerSettings::apiWfs3MaxLimit() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_API_WFS3_MAX_LIMIT ).toLongLong();
}

View File

@ -64,7 +64,9 @@ class SERVER_EXPORT QgsServerSettingsEnv : public QObject
QGIS_SERVER_SHOW_GROUP_SEPARATOR, //! Show group (thousands) separator when formatting numeric values, defaults to FALSE (since QGIS 3.8)
QGIS_SERVER_OVERRIDE_SYSTEM_LOCALE, //! Override system locale (since QGIS 3.8)
QGIS_SERVER_WMS_MAX_HEIGHT, //! Maximum height for a WMS request. The most conservative between this and the project one is used (since QGIS 3.8)
QGIS_SERVER_WMS_MAX_WIDTH //! Maximum width for a WMS request. The most conservative between this and the project one is used (since QGIS 3.8)
QGIS_SERVER_WMS_MAX_WIDTH, //! Maximum width for a WMS request. The most conservative between this and the project one is used (since QGIS 3.8)
QGIS_SERVER_API_RESOURCES_DIRECTORY, //! Base directory where HTML templates and static assets (e.g. images, js and css files) are searched for (since QGIS 3.10).
QGIS_SERVER_API_WFS3_MAX_LIMIT //! Maximum value for "limit" in a features request, defaults to 10000 (since QGIS 3.10).
};
Q_ENUM( EnvVar )
};
@ -200,6 +202,26 @@ class SERVER_EXPORT QgsServerSettings
*/
int wmsMaxWidth() const;
/**
* Returns the server-wide base directory where HTML templates and static assets (e.g. images, js and css files) are searched for.
*
* The default path is calculated by joining QgsApplication::pkgDataPath() with "resources/server/api", this path
* can be changed by setting the environment variable QGIS_SERVER_API_RESOURCES_DIRECTORY.
*
* \since QGIS 3.10
*/
QString apiResourcesDirectory() const;
/**
* Returns the server-wide maximum allowed value for \"limit\" in a features request.
*
* The default value is 10000, this value can be changed by setting the environment
* variable QGIS_SERVER_API_WFS3_MAX_LIMIT.
*
* \since QGIS 3.10
*/
qlonglong apiWfs3MaxLimit() const;
private:
void initSettings();
QVariant value( QgsServerSettingsEnv::EnvVar envVar ) const;

View File

@ -19,6 +19,7 @@
#include "qgsserviceregistry.h"
#include "qgsservice.h"
#include "qgsserverapi.h"
#include "qgsmessagelog.h"
#include <algorithm>
@ -91,8 +92,8 @@ QgsService *QgsServiceRegistry::getService( const QString &name, const QString &
QString key;
// Check that we have a service of that name
VersionTable::const_iterator v = mVersions.constFind( name );
if ( v != mVersions.constEnd() )
VersionTable::const_iterator v = mServiceVersions.constFind( name );
if ( v != mServiceVersions.constEnd() )
{
key = version.isEmpty() ? v->second : makeServiceKey( name, version );
ServiceTable::const_iterator it = mServices.constFind( key );
@ -102,7 +103,7 @@ QgsService *QgsServiceRegistry::getService( const QString &name, const QString &
}
else
{
// Return the dofault version
// Return the default version
QgsMessageLog::logMessage( QString( "Service %1 %2 not found, returning default" ).arg( name, version ) );
service = mServices[v->second].get();
}
@ -123,11 +124,11 @@ void QgsServiceRegistry::registerService( QgsService *service )
QString key = makeServiceKey( name, version );
if ( mServices.constFind( key ) != mServices.constEnd() )
{
QgsMessageLog::logMessage( QString( "Error Service %1 %2 is already registered" ).arg( name, version ) );
QgsMessageLog::logMessage( QStringLiteral( "Error Service %1 %2 is already registered" ).arg( name, version ) );
return;
}
QgsMessageLog::logMessage( QString( "Adding service %1 %2" ).arg( name, version ) );
QgsMessageLog::logMessage( QStringLiteral( "Adding service %1 %2" ).arg( name, version ) );
mServices.insert( key, std::shared_ptr<QgsService>( service ) );
// Check the default version
@ -135,11 +136,11 @@ void QgsServiceRegistry::registerService( QgsService *service )
// is the default one.
// this will ensure that native services are always
// the defaults.
VersionTable::const_iterator v = mVersions.constFind( name );
if ( v == mVersions.constEnd() )
VersionTable::const_iterator v = mServiceVersions.constFind( name );
if ( v == mServiceVersions.constEnd() )
{
// Insert the service as the default one
mVersions.insert( name, VersionTable::mapped_type( version, key ) );
mServiceVersions.insert( name, VersionTable::mapped_type( version, key ) );
}
/*
if ( v != mVersions.constEnd() )
@ -158,12 +159,121 @@ void QgsServiceRegistry::registerService( QgsService *service )
}
int QgsServiceRegistry::unregisterApi( const QString &name, const QString &version )
{
// Check that we have an API of that name
int removed = 0;
VersionTable::const_iterator v = mApiVersions.constFind( name );
if ( v != mApiVersions.constEnd() )
{
if ( version.isEmpty() )
{
// No version specified, remove all versions
ApiTable::iterator it = mApis.begin();
while ( it != mApis.end() )
{
if ( ( *it )->name() == name )
{
QgsMessageLog::logMessage( QString( "Unregistering API %1 %2" ).arg( name, ( *it )->version() ) );
it = mApis.erase( it );
++removed;
}
else
{
++it;
}
}
// Remove from version table
mApiVersions.remove( name );
}
else
{
const QString key = makeServiceKey( name, version );
ApiTable::iterator found = mApis.find( key );
if ( found != mApis.end() )
{
QgsMessageLog::logMessage( QString( "Unregistering API %1 %2" ).arg( name, version ) );
mApis.erase( found );
removed = 1;
// Find if we have other services of that name
// but with different version
//
QString maxVer;
std::function < void ( const ApiTable::mapped_type & ) >
findGreaterVersion = [name, &maxVer]( const ApiTable::mapped_type & api )
{
if ( api->name() == name &&
( maxVer.isEmpty() || isVersionGreater( api->version(), maxVer ) ) )
maxVer = api->version();
};
mApiVersions.remove( name );
std::for_each( mApis.constBegin(), mApis.constEnd(), findGreaterVersion );
if ( !maxVer.isEmpty() )
{
// Set the new default service
const QString key = makeServiceKey( name, maxVer );
mApiVersions.insert( name, VersionTable::mapped_type( version, key ) );
}
}
}
}
return removed;
}
QgsServerApi *QgsServiceRegistry::apiForRequest( const QgsServerRequest &request ) const
{
for ( const auto &api : mApis )
{
QgsMessageLog::logMessage( QStringLiteral( "Trying URL path: %1 for %2" ).arg( request.url().path(), api->rootPath() ), QStringLiteral( "Server" ), Qgis::Info );
if ( api->accept( request.url() ) )
{
Q_ASSERT( !api->name().isEmpty() );
QgsMessageLog::logMessage( QStringLiteral( "API %1 accepts the URL path %2 " ).arg( api->name(), request.url().path() ), QStringLiteral( "Server" ), Qgis::Info );
return api.get();
}
}
return nullptr;
}
QgsServerApi *QgsServiceRegistry::getApi( const QString &name, const QString &version )
{
QgsServerApi *api = nullptr;
QString key;
// Check that we have an API of that name
VersionTable::const_iterator v = mApiVersions.constFind( name );
if ( v != mApiVersions.constEnd() )
{
key = version.isEmpty() ? v->second : makeServiceKey( name, version );
ApiTable::const_iterator it = mApis.constFind( key );
if ( it != mApis.constEnd() )
{
api = it->get();
}
else
{
// Return the default version
QgsMessageLog::logMessage( QString( "API %1 %2 not found, returning default" ).arg( name, version ) );
api = mApis[v->second].get();
}
}
else
{
QgsMessageLog::logMessage( QString( "API %1 is not registered" ).arg( name ) );
}
return api;
}
int QgsServiceRegistry::unregisterService( const QString &name, const QString &version )
{
// Check that we have a service of that name
int removed = 0;
VersionTable::const_iterator v = mVersions.constFind( name );
if ( v != mVersions.constEnd() )
VersionTable::const_iterator v = mServiceVersions.constFind( name );
if ( v != mServiceVersions.constEnd() )
{
if ( version.isEmpty() )
{
@ -183,7 +293,7 @@ int QgsServiceRegistry::unregisterService( const QString &name, const QString &v
}
}
// Remove from version table
mVersions.remove( name );
mServiceVersions.remove( name );
}
else
{
@ -207,14 +317,14 @@ int QgsServiceRegistry::unregisterService( const QString &name, const QString &v
maxVer = service->version();
};
mVersions.remove( name );
mServiceVersions.remove( name );
std::for_each( mServices.constBegin(), mServices.constEnd(), findGreaterVersion );
if ( !maxVer.isEmpty() )
{
// Set the new default service
QString key = makeServiceKey( name, maxVer );
mVersions.insert( name, VersionTable::mapped_type( version, key ) );
mServiceVersions.insert( name, VersionTable::mapped_type( version, key ) );
}
}
}
@ -230,9 +340,39 @@ void QgsServiceRegistry::init( const QString &nativeModulePath, QgsServerInterfa
void QgsServiceRegistry::cleanUp()
{
// Release all services
mVersions.clear();
mServiceVersions.clear();
mServices.clear();
mApis.clear();
mNativeLoader.unloadModules();
}
bool QgsServiceRegistry::registerApi( QgsServerApi *api )
{
const QString name = api->name();
const QString version = api->version();
// Test if service is already registered
const QString key = makeServiceKey( name, version );
if ( mApis.constFind( key ) != mApis.constEnd() )
{
QgsMessageLog::logMessage( QStringLiteral( "Error API %1 %2 is already registered" ).arg( name, version ) );
return false;
}
QgsMessageLog::logMessage( QStringLiteral( "Adding API %1 %2" ).arg( name, version ) );
mApis.insert( key, std::shared_ptr<QgsServerApi>( api ) );
// Check the default version
// The first inserted service of a given name
// is the default one.
// this will ensure that native services are always
// the defaults.
VersionTable::const_iterator v = mApiVersions.constFind( name );
if ( v == mApiVersions.constEnd() )
{
// Insert the service as the default one
mApiVersions.insert( name, VersionTable::mapped_type( version, key ) );
}
return true;
}

View File

@ -29,6 +29,8 @@
#include <memory>
class QgsService;
class QgsServerRequest;
class QgsServerApi;
class QgsServerInterface;
/**
@ -71,16 +73,55 @@ class SERVER_EXPORT QgsServiceRegistry
* This method is intended to be called by modules for registering
* services. A module may register multiple services.
*
* The registry gain ownership of services and will call 'delete' on cleanup
* The registry takes ownership of services and will call 'delete' on cleanup
*
* \param service a QgsService to be registered
*/
void registerService( QgsService *service SIP_TRANSFER );
/**
* Registers the QgsServerApi \a api
*
* The registry takes ownership of services and will call 'delete' on cleanup
* \since QGIS 3.10
*/
bool registerApi( QgsServerApi *api SIP_TRANSFER );
/**
* Unregisters API from its name and version
*
* \param name the name of the service
* \param version (optional) the specific version to unload
* \returns the number of APIs unregistered
*
* If the version is not specified then all versions from the specified API
* are unloaded
* \since QGIS 3.10
*/
int unregisterApi( const QString &name, const QString &version = QString() );
/**
* Searches the API register for an API matching the \a request and returns a (possibly NULL) pointer to it.
* \since QGIS 3.10
*/
QgsServerApi *apiForRequest( const QgsServerRequest &request ) const SIP_SKIP;
/**
* Retrieves an API from its name
*
* If the version is not provided the higher version of the service is returned
*
* \param name the name of the API
* \param version the version string (optional)
* \returns QgsServerApi
* \since QGIS 3.10
*/
QgsServerApi *getApi( const QString &name, const QString &version = QString() );
/**
* Unregister service from its name and version
*
* \param name the tame of the service
* \param name the name of the service
* \param version (optional) the specific version to unload
* \returns the number of services unregistered
*
@ -102,15 +143,20 @@ class SERVER_EXPORT QgsServiceRegistry
void cleanUp();
private:
// XXX consider using QMap because of the few numbers of
// elements to handle
typedef QHash<QString, std::shared_ptr<QgsService> > ServiceTable;
typedef QHash<QString, std::shared_ptr<QgsServerApi> > ApiTable;
typedef QHash<QString, QPair<QString, QString> > VersionTable;
QgsServiceNativeLoader mNativeLoader;
ServiceTable mServices;
VersionTable mVersions;
VersionTable mServiceVersions;
ApiTable mApis;
VersionTable mApiVersions;
};
#endif

View File

@ -9,6 +9,7 @@ SET (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${QGIS_OUTPUT_DIRECTORY}/${QGIS_SERVER_MODUL
ADD_SUBDIRECTORY(DummyService)
ADD_SUBDIRECTORY(wms)
ADD_SUBDIRECTORY(wfs)
ADD_SUBDIRECTORY(wfs3)
ADD_SUBDIRECTORY(wcs)
ADD_SUBDIRECTORY(wmts)

View File

@ -13,6 +13,7 @@ ADD_LIBRARY (dummy MODULE ${dummy_SRCS})
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/gui
${CMAKE_BINARY_DIR}/src/python

View File

@ -20,6 +20,7 @@
#include "qgsservicemodule.h"
#include "qgsserviceregistry.h"
#include "qgsservice.h"
#include "qgsserverapi.h"
#include "qgsserverinterface.h"
#include "qgslogger.h"
#include "qgsmessagelog.h"

View File

@ -22,6 +22,7 @@ INCLUDE_DIRECTORIES(SYSTEM
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/python
${CMAKE_BINARY_DIR}/src/analysis

View File

@ -32,6 +32,7 @@ INCLUDE_DIRECTORIES(SYSTEM
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/python
${CMAKE_BINARY_DIR}/src/analysis

View File

@ -0,0 +1,62 @@
########################################################
# Files
SET (wfs3_SRCS
${CMAKE_SOURCE_DIR}/external/nlohmann/json.hpp
${CMAKE_SOURCE_DIR}/external/inja/inja.hpp
qgswfs3.cpp
qgswfs3handlers.cpp
)
SET (wfs3_MOC_HDRS
)
########################################################
# Build
QT5_WRAP_CPP(wfs3_MOC_SRCS ${wfs3_MOC_HDRS})
ADD_LIBRARY (wfs3 MODULE ${wfs3_SRCS} ${wfs3_MOC_SRCS} ${wfs3_MOC_HDRS})
INCLUDE_DIRECTORIES(SYSTEM
${GDAL_INCLUDE_DIR}
${POSTGRES_INCLUDE_DIR}
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/python
${CMAKE_BINARY_DIR}/src/analysis
${CMAKE_BINARY_DIR}/src/server
${CMAKE_CURRENT_BINARY_DIR}
../../../core
../../../core/dxf
../../../core/expression
../../../core/geometry
../../../core/metadata
../../../core/raster
../../../core/symbology
../../../core/layertree
../../../core/fieldformatter
../..
..
.
)
TARGET_LINK_LIBRARIES(wfs3
qgis_core
qgis_server
)
########################################################
# Install
INSTALL(TARGETS wfs3
RUNTIME DESTINATION ${QGIS_SERVER_MODULE_DIR}
LIBRARY DESTINATION ${QGIS_SERVER_MODULE_DIR}
)

View File

@ -0,0 +1,663 @@
{
"openapi" : "3.0.1",
"info" : {
"title" : "A sample API conforming to the OGC Web Feature Service standard",
"description" : "This is a sample OpenAPI definition that conforms to the OGC Web Feature Service specification (conformance classes: \"Core\", \"GeoJSON\", \"HTML\" and \"OpenAPI 3.0\").",
"contact" : {
"name" : "Acme Corporation",
"url" : "http://example.org/",
"email" : "info@example.org"
},
"license" : {
"name" : "CC-BY 4.0 license",
"url" : "https://creativecommons.org/licenses/by/4.0/"
},
"version" : "M1"
},
"servers" : [ {
"url" : "https://dev.example.org/",
"description" : "Development server"
}, {
"url" : "https://data.example.org/",
"description" : "Production server"
} ],
"tags" : [ {
"name" : "Capabilities",
"description" : "Essential characteristics of this API including information about the data."
}, {
"name" : "Features",
"description" : "Access to data (features)."
} ],
"paths" : {
"/" : {
"get" : {
"tags" : [ "Capabilities" ],
"summary" : "landing page of this API",
"description" : "The landing page provides links to the API definition, the Conformance statements and the metadata about the feature data in this dataset.",
"operationId" : "getLandingPage",
"responses" : {
"200" : {
"description" : "links to the API capabilities",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/root"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
},
"/conformance" : {
"get" : {
"tags" : [ "Capabilities" ],
"summary" : "information about standards that this API conforms to",
"description" : "list all requirements classes specified in a standard (e.g., WFS 3.0 Part 1: Core) that the server conforms to",
"operationId" : "getRequirementsClasses",
"responses" : {
"200" : {
"description" : "the URIs of all requirements classes supported by the server",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/req-classes"
}
}
}
},
"default" : {
"description" : "An error occurred.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/exception"
}
}
}
}
}
}
},
"/collections" : {
"get" : {
"tags" : [ "Capabilities" ],
"summary" : "describe the feature collections in the dataset",
"operationId" : "describeCollections",
"responses" : {
"200" : {
"description" : "Metdata about the feature collections shared by this API.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/content"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
},
"default" : {
"description" : "An error occurred.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/exception"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
},
"/collections/{collectionId}" : {
"get" : {
"tags" : [ "Capabilities" ],
"summary" : "describe the {collectionId} feature collection",
"operationId" : "describeCollection",
"parameters" : [ {
"name" : "collectionId",
"in" : "path",
"description" : "Identifier (name) of a specific collection",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"description" : "Metadata about the {collectionId} collection shared by this API.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/collectionInfo"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
},
"default" : {
"description" : "An error occurred.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/exception"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
},
"/collections/{collectionId}/items" : {
"get" : {
"tags" : [ "Features" ],
"summary" : "retrieve features of feature collection {collectionId}",
"description" : "Every feature in a dataset belongs to a collection. A dataset may consist of multiple feature collections. A feature collection is often a collection of features of a similar type, based on a common schema.\\\nUse content negotiation to request HTML or GeoJSON.",
"operationId" : "getFeatures",
"parameters" : [ {
"name" : "collectionId",
"in" : "path",
"description" : "Identifier (name) of a specific collection",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
}, {
"name" : "limit",
"in" : "query",
"description" : "The optional limit parameter limits the number of items that are\npresented in the response document.\n\nOnly items are counted that are on the first level of the collection in\nthe response document. Nested objects contained within the explicitly\nrequested items shall not be counted.\n\n* Minimum = 1\n* Maximum = 10000\n* Default = 10\n",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maximum" : 10000,
"minimum" : 1,
"type" : "integer",
"default" : 10
}
}, {
"name" : "bbox",
"in" : "query",
"description" : "Only features that have a geometry that intersects the bounding box are selected. The bounding box is provided as four or six numbers, depending on whether the coordinate reference system includes a vertical axis (elevation or depth):\n* Lower left corner, coordinate axis 1 * Lower left corner, coordinate axis 2 * Lower left corner, coordinate axis 3 (optional) * Upper right corner, coordinate axis 1 * Upper right corner, coordinate axis 2 * Upper right corner, coordinate axis 3 (optional)\nThe coordinate reference system of the values is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`.\nFor WGS84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge).\nIf a feature has multiple spatial geometry properties, it is the decision of the server whether only a single spatial geometry property is used to determine the extent or all relevant geometries.\n",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maxItems" : 6,
"minItems" : 4,
"type" : "array",
"items" : {
"type" : "number"
}
}
}, {
"name" : "time",
"in" : "query",
"description" : "Either a date-time or a period string that adheres to RFC 3339. Examples:\n* A date-time: \"2018-02-12T23:20:50Z\" * A period: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\" or \"2018-02-12T00:00:00Z/P1M6DT12H31M12S\"\nOnly features that have a temporal property that intersects the value of `time` are selected.\nIf a feature has multiple temporal properties, it is the decision of the server whether only a single temporal property is used to determine the extent or all relevant temporal properties.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"description" : "Information about the feature collection plus the first features matching the selection parameters.",
"content" : {
"application/geo+json" : {
"schema" : {
"$ref" : "#/components/schemas/featureCollectionGeoJSON"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
},
"default" : {
"description" : "An error occurred.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/exception"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
},
"/collections/{collectionId}/items/{featureId}" : {
"get" : {
"tags" : [ "Features" ],
"summary" : "retrieve a feature; use content negotiation to request HTML or GeoJSON",
"operationId" : "getFeature",
"parameters" : [ {
"name" : "collectionId",
"in" : "path",
"description" : "Identifier (name) of a specific collection",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
}, {
"name" : "featureId",
"in" : "path",
"description" : "Local identifier of a specific feature",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
} ],
"responses" : {
"200" : {
"description" : "A feature.",
"content" : {
"application/geo+json" : {
"schema" : {
"$ref" : "#/components/schemas/featureGeoJSON"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
},
"default" : {
"description" : "An error occurred.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/exception"
}
},
"text/html" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
}
},
"components" : {
"schemas" : {
"exception" : {
"required" : [ "code" ],
"type" : "object",
"properties" : {
"code" : {
"type" : "string"
},
"description" : {
"type" : "string"
}
}
},
"root" : {
"required" : [ "links" ],
"type" : "object",
"properties" : {
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/",
"rel" : "self",
"type" : "application/json",
"title" : "this document"
}, {
"href" : "http://data.example.org/api",
"rel" : "service",
"type" : "application/openapi+json;version=3.0",
"title" : "the API definition"
}, {
"href" : "http://data.example.org/conformance",
"rel" : "conformance",
"type" : "application/json",
"title" : "WFS 3.0 conformance classes implemented by this server"
}, {
"href" : "http://data.example.org/collections",
"rel" : "data",
"type" : "application/json",
"title" : "Metadata about the feature collections"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
}
}
},
"req-classes" : {
"required" : [ "conformsTo" ],
"type" : "object",
"properties" : {
"conformsTo" : {
"type" : "array",
"example" : [ "http://www.opengis.net/spec/wfs-1/3.0/req/core", "http://www.opengis.net/spec/wfs-1/3.0/req/oas30", "http://www.opengis.net/spec/wfs-1/3.0/req/html", "http://www.opengis.net/spec/wfs-1/3.0/req/geojson" ],
"items" : {
"type" : "string"
}
}
}
},
"link" : {
"required" : [ "href" ],
"type" : "object",
"properties" : {
"href" : {
"type" : "string"
},
"rel" : {
"type" : "string",
"example" : "prev"
},
"type" : {
"type" : "string",
"example" : "application/geo+json"
},
"hreflang" : {
"type" : "string",
"example" : "en"
}
}
},
"content" : {
"required" : [ "collections", "links" ],
"type" : "object",
"properties" : {
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/collections.json",
"rel" : "self",
"type" : "application/json",
"title" : "this document"
}, {
"href" : "http://data.example.org/collections.html",
"rel" : "alternate",
"type" : "text/html",
"title" : "this document as HTML"
}, {
"href" : "http://schemas.example.org/1.0/foobar.xsd",
"rel" : "describedBy",
"type" : "application/xml",
"title" : "XML schema for Acme Corporation data"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"collections" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/collectionInfo"
}
}
}
},
"collectionInfo" : {
"required" : [ "links", "name" ],
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "identifier of the collection used, for example, in URIs",
"example" : "buildings"
},
"title" : {
"type" : "string",
"description" : "human readable title of the collection",
"example" : "Buildings"
},
"description" : {
"type" : "string",
"description" : "a description of the features in the collection",
"example" : "Buildings in the city of Bonn."
},
"links" : {
"type" : "array",
"example" : [ {
"href" : "http://data.example.org/collections/buildings/items",
"rel" : "item",
"type" : "application/geo+json",
"title" : "Buildings"
}, {
"href" : "http://example.org/concepts/building.html",
"rel" : "describedBy",
"type" : "text/html",
"title" : "Feature catalogue for buildings"
} ],
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"extent" : {
"$ref" : "#/components/schemas/extent"
},
"crs" : {
"type" : "array",
"description" : "The coordinate reference systems in which geometries may be retrieved. Coordinate reference systems are identified by a URI. The first coordinate reference system is the coordinate reference system that is used by default. This is always \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", i.e. WGS84 longitude/latitude.",
"items" : {
"type" : "string"
},
"default" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ]
}
}
},
"extent" : {
"type" : "object",
"properties" : {
"crs" : {
"type" : "string",
"description" : "Coordinate reference system of the coordinates in the spatial extent (property `spatial`). In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional coordinate reference systems.",
"default" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"enum" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ]
},
"spatial" : {
"maxItems" : 6,
"minItems" : 4,
"type" : "array",
"description" : "West, north, east, south edges of the spatial extent. The minimum and maximum values apply to the coordinate reference system WGS84 longitude/latitude that is supported in the Core. If, for example, a projected coordinate reference system is used, the minimum and maximum values need to be adjusted.",
"example" : [ -180, -90, 180, 90 ],
"items" : {
"type" : "number"
}
},
"trs" : {
"type" : "string",
"description" : "Temporal reference system of the coordinates in the temporal extent (property `temporal`). In the Core, only the Gregorian calendar is supported. Extensions may support additional temporal reference systems.",
"default" : "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian",
"enum" : [ "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" ]
},
"temporal" : {
"maxItems" : 2,
"minItems" : 2,
"type" : "array",
"description" : "Begin and end times of the temporal extent.",
"example" : [ "2011-11-11T12:22:11Z", "2012-11-24T12:32:43Z" ],
"items" : {
"type" : "string",
"format" : "dateTime"
}
}
}
},
"featureCollectionGeoJSON" : {
"required" : [ "features", "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "FeatureCollection" ]
},
"features" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/featureGeoJSON"
}
},
"links" : {
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/link"
}
},
"timeStamp" : {
"type" : "string",
"format" : "dateTime"
},
"numberMatched" : {
"minimum" : 0,
"type" : "integer"
},
"numberReturned" : {
"minimum" : 0,
"type" : "integer"
}
}
},
"featureGeoJSON" : {
"required" : [ "geometry", "properties", "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "Feature" ]
},
"geometry" : {
"$ref" : "#/components/schemas/geometryGeoJSON"
},
"properties" : {
"type" : "object",
"nullable" : true
},
"id" : {
"oneOf" : [ {
"type" : "string"
}, {
"type" : "integer"
} ]
}
}
},
"geometryGeoJSON" : {
"required" : [ "type" ],
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : [ "Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon", "GeometryCollection" ]
}
}
}
},
"parameters" : {
"limit" : {
"name" : "limit",
"in" : "query",
"description" : "The optional limit parameter limits the number of items that are\npresented in the response document.\n\nOnly items are counted that are on the first level of the collection in\nthe response document. Nested objects contained within the explicitly\nrequested items shall not be counted.\n\n* Minimum = 1\n* Maximum = 10000\n* Default = 10\n",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maximum" : 10000,
"minimum" : 1,
"type" : "integer",
"default" : 10
}
},
"bbox" : {
"name" : "bbox",
"in" : "query",
"description" : "Only features that have a geometry that intersects the bounding box are selected. The bounding box is provided as four or six numbers, depending on whether the coordinate reference system includes a vertical axis (elevation or depth):\n* Lower left corner, coordinate axis 1 * Lower left corner, coordinate axis 2 * Lower left corner, coordinate axis 3 (optional) * Upper right corner, coordinate axis 1 * Upper right corner, coordinate axis 2 * Upper right corner, coordinate axis 3 (optional)\nThe coordinate reference system of the values is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`.\nFor WGS84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge).\nIf a feature has multiple spatial geometry properties, it is the decision of the server whether only a single spatial geometry property is used to determine the extent or all relevant geometries.\n",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"maxItems" : 6,
"minItems" : 4,
"type" : "array",
"items" : {
"type" : "number"
}
}
},
"time" : {
"name" : "time",
"in" : "query",
"description" : "Either a date-time or a period string that adheres to RFC 3339. Examples:\n* A date-time: \"2018-02-12T23:20:50Z\" * A period: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\" or \"2018-02-12T00:00:00Z/P1M6DT12H31M12S\"\nOnly features that have a temporal property that intersects the value of `time` are selected.\nIf a feature has multiple temporal properties, it is the decision of the server whether only a single temporal property is used to determine the extent or all relevant temporal properties.",
"required" : false,
"style" : "form",
"explode" : false,
"schema" : {
"type" : "string"
}
},
"collectionId" : {
"name" : "collectionId",
"in" : "path",
"description" : "Identifier (name) of a specific collection",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
},
"featureId" : {
"name" : "featureId",
"in" : "path",
"description" : "Local identifier of a specific feature",
"required" : true,
"style" : "simple",
"explode" : false,
"schema" : {
"type" : "string"
}
}
}
}
}

View File

@ -0,0 +1,65 @@
/***************************************************************************
qgswfs3.cpp
-------------------------
begin : April 15, 2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 "qgsmodule.h"
#include "qgsserverogcapi.h"
#include "qgswfs3handlers.h"
/**
* \ingroup server
* \class QgsWfsModule
* \brief Module specialized for WFS3 service
* \since QGIS 3.10
*/
class QgsWfs3Module: public QgsServiceModule
{
public:
void registerSelf( QgsServiceRegistry &registry, QgsServerInterface *serverIface ) override
{
QgsServerOgcApi *wfs3Api = new QgsServerOgcApi { serverIface,
QStringLiteral( "/wfs3" ),
QStringLiteral( "OGC WFS3 (Draft)" ),
QStringLiteral( "1.0.0" )
};
// Register handlers
wfs3Api->registerHandler<QgsWfs3CollectionsItemsHandler>();
wfs3Api->registerHandler<QgsWfs3CollectionsFeatureHandler>();
wfs3Api->registerHandler<QgsWfs3CollectionsHandler>();
wfs3Api->registerHandler<QgsWfs3DescribeCollectionHandler>();
wfs3Api->registerHandler<QgsWfs3ConformanceHandler>();
wfs3Api->registerHandler<QgsWfs3StaticHandler>();
// API handler must access to the whole API
wfs3Api->registerHandler<QgsWfs3APIHandler>( wfs3Api );
wfs3Api->registerHandler<QgsWfs3LandingPageHandler>();
// Register API
registry.registerApi( wfs3Api );
}
};
// Entry points
QGISEXTERN QgsServiceModule *QGS_ServiceModule_Init()
{
static QgsWfs3Module module;
return &module;
}
QGISEXTERN void QGS_ServiceModule_Exit( QgsServiceModule * )
{
// Nothing to do
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,239 @@
/***************************************************************************
qgswfs3handlers.h
-------------------------
begin : May 3, 2019
copyright : (C) 2019 by Alessandro Pasotti
email : elpaso at itopen dot it
***************************************************************************/
/***************************************************************************
* *
* 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 QGS_WFS3_HANDLERS_H
#define QGS_WFS3_HANDLERS_H
#include "qgsserverogcapihandler.h"
class QgsServerOgcApi;
/**
* The APIHandler class Wfs3handles the API definition
*/
class QgsWfs3APIHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3APIHandler( const QgsServerOgcApi *api );
// QgsServerOgcApiHandler interface
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/api)re" ); }
std::string operationId() const override { return "getApiDescription"; }
std::string summary() const override { return "The API definition"; }
std::string description() const override { return "The formal documentation of this API according to the OpenAPI specification, version 3.0. I.e., this document."; }
std::string linkTitle() const override { return "API definition"; }
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::OPENAPI3, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::service_desc; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::OPENAPI3; }
json schema( const QgsServerApiContext &context ) const override;
private:
const QgsServerOgcApi *mApi = nullptr;
};
/**
* The StaticHandler class Wfs3 serves static files from the static path (resources/server/api/wfs3/static)
* \see staticPath()
*/
class QgsWfs3StaticHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3StaticHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(/static/(?<staticFilePath>.*)$)re" ); }
std::string operationId() const override { return "static"; }
std::string summary() const override { return "Serves static files"; }
std::string description() const override { return "Serves static files"; }
std::string linkTitle() const override { return "Serves static files"; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::HTML; }
};
class QgsWfs3LandingPageHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3LandingPageHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re((.html|.json)?$)re" ); }
std::string operationId() const override { return "getLandingPage"; }
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
std::string summary() const override
{
return "WFS 3.0 Landing Page";
}
std::string description() const override
{
return "The landing page provides links to the API definition, the Conformance "
"statements and the metadata about the feature data in this dataset.";
}
std::string linkTitle() const override { return "Landing page"; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::self; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::JSON; }
json schema( const QgsServerApiContext &context ) const override;
};
class QgsWfs3ConformanceHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3ConformanceHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(/conformance)re" ); }
std::string operationId() const override { return "getRequirementClasses"; }
std::string summary() const override { return "Information about standards that this API conforms to"; }
std::string description() const override
{
return "List all requirements classes specified in a standard (e.g., WFS 3.0 "
"Part 1: Core) that the server conforms to";
}
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
std::string linkTitle() const override { return "WFS 3.0 conformance classes"; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::conformance; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::JSON; }
json schema( const QgsServerApiContext &context ) const override;
};
/**
* The CollectionsHandler lists all available collections for the current project
* Path: /collections
*/
class QgsWfs3CollectionsHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3CollectionsHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(/collections(\.json|\.html)?$)re" ); }
std::string operationId() const override { return "describeCollections"; }
std::string summary() const override
{
return "Metadata about the feature collections shared by this API.";
}
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
std::string description() const override
{
return "Describe the feature collections in the dataset "
"statements and the metadata about the feature data in this dataset.";
}
std::string linkTitle() const override { return "Feature collections"; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::JSON; }
json schema( const QgsServerApiContext &context ) const override;
};
/**
* The DescribeCollectionHandler describes a single collection
* Path: /collections/{collectionId}
*/
class QgsWfs3DescribeCollectionHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3DescribeCollectionHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+?)(\.json|\.html)?$)re" ); }
std::string operationId() const override { return "describeCollection"; }
std::string summary() const override { return "Describe the feature collection"; }
std::string description() const override { return "Metadata about a feature collection."; }
std::string linkTitle() const override { return "Feature collection"; }
QStringList tags() const override { return { QStringLiteral( "Capabilities" ) }; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::JSON; }
json schema( const QgsServerApiContext &context ) const override;
};
/**
* The CollectionsItemsHandler list all items in the collection
* Path: /collections/{collectionId}
*/
class QgsWfs3CollectionsItemsHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3CollectionsItemsHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+)/items(\.geojson|\.json|\.html)?$)re" ); }
std::string operationId() const override { return "getFeatures"; }
std::string summary() const override { return "Retrieve features of feature collection collectionId"; }
std::string description() const override
{
return "Every feature in a dataset belongs to a collection. A dataset may "
"consist of multiple feature collections. A feature collection is often a "
"collection of features of a similar type, based on a common schema. "
"Use content negotiation or specify a file extension to request HTML (.html) "
"or GeoJSON (.json).";
}
std::string linkTitle() const override { return "Retrieve the features of the collection"; }
QStringList tags() const override { return { QStringLiteral( "Features" ) }; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::GEOJSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::GEOJSON; }
QList<QgsServerQueryStringParameter> parameters( const QgsServerApiContext &context ) const override;
json schema( const QgsServerApiContext &context ) const override;
private:
// Retrieve the fields filter parameters
const QList<QgsServerQueryStringParameter> fieldParameters( const QgsVectorLayer *mapLayer ) const;
};
class QgsWfs3CollectionsFeatureHandler: public QgsServerOgcApiHandler
{
public:
QgsWfs3CollectionsFeatureHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
QRegularExpression path() const override { return QRegularExpression( R"re(/collections/(?<collectionId>[^/]+)/items/(?<featureId>[^/]+?)(\.json|\.geojson|\.html)?$)re" ); }
std::string operationId() const override { return "getFeature"; }
std::string description() const override { return "Retrieve a feature; use content negotiation or specify a file extension to request HTML (.html or GeoJSON (.json)"; }
std::string summary() const override { return "Retrieve a single feature"; }
std::string linkTitle() const override { return "Retrieve a feature"; }
QStringList tags() const override { return { QStringLiteral( "Features" ) }; }
QList<QgsServerOgcApi::ContentType> contentTypes() const override { return { QgsServerOgcApi::ContentType::GEOJSON, QgsServerOgcApi::ContentType::HTML }; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::data; }
QgsServerOgcApi::ContentType defaultContentType() const override { return QgsServerOgcApi::ContentType::GEOJSON; }
json schema( const QgsServerApiContext &context ) const override;
};
#endif // QGS_WFS3_HANDLERS_H

View File

@ -42,6 +42,7 @@ INCLUDE_DIRECTORIES(SYSTEM
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/annotations
${CMAKE_SOURCE_DIR}/src/core/expression

View File

@ -29,6 +29,7 @@ INCLUDE_DIRECTORIES(SYSTEM
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/python
${CMAKE_BINARY_DIR}/src/analysis

View File

@ -1,4 +1,4 @@
/***************************************************************************
/**************************************************************************
test_template.cpp
--------------------------------------
Date : Sun Sep 16 12:22:23 AKDT 2007

View File

@ -293,6 +293,8 @@ IF (WITH_SERVER)
ADD_PYTHON_TEST(PyQgsServerLogger test_qgsserverlogger.py)
ADD_PYTHON_TEST(PyQgsServerPlugins test_qgsserver_plugins.py)
ADD_PYTHON_TEST(PyQgsServerWMS test_qgsserver_wms.py)
ADD_PYTHON_TEST(PyQgsServerApi test_qgsserver_api.py)
ADD_PYTHON_TEST(PyQgsServerApiContext test_qgsserver_apicontext.py)
ADD_PYTHON_TEST(PyQgsServerWMSGetMap test_qgsserver_wms_getmap.py)
ADD_PYTHON_TEST(PyQgsServerWMSGetMapSizeProject test_qgsserver_wms_getmap_size_project.py)
ADD_PYTHON_TEST(PyQgsServerWMSGetMapSizeServer test_qgsserver_wms_getmap_size_server.py)

View File

@ -0,0 +1,590 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsServer API.
.. 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__ = 'Alessandro Pasotti'
__date__ = '17/04/2019'
__copyright__ = 'Copyright 2019, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import os
import json
import re
# Deterministic XML
os.environ['QT_HASH_SEED'] = '1'
from qgis.server import (
QgsBufferServerRequest,
QgsBufferServerResponse,
QgsServerApi,
QgsServerApiBadRequestException,
QgsServerQueryStringParameter,
QgsServerApiContext,
QgsServerOgcApi,
QgsServerOgcApiHandler,
QgsServerApiUtils,
QgsServiceRegistry
)
from qgis.core import QgsProject, QgsRectangle
from qgis.PyQt import QtCore
from qgis.testing import unittest
from utilities import unitTestDataPath
from urllib import parse
import tempfile
from test_qgsserver import QgsServerTestBase
class QgsServerAPIUtilsTest(QgsServerTestBase):
""" QGIS API server utils tests"""
def test_parse_bbox(self):
bbox = QgsServerApiUtils.parseBbox('8.203495,44.901482,8.203497,44.901484')
self.assertEquals(bbox.xMinimum(), 8.203495)
self.assertEquals(bbox.yMinimum(), 44.901482)
self.assertEquals(bbox.xMaximum(), 8.203497)
self.assertEquals(bbox.yMaximum(), 44.901484)
bbox = QgsServerApiUtils.parseBbox('8.203495,44.901482,100,8.203497,44.901484,120')
self.assertEquals(bbox.xMinimum(), 8.203495)
self.assertEquals(bbox.yMinimum(), 44.901482)
self.assertEquals(bbox.xMaximum(), 8.203497)
self.assertEquals(bbox.yMaximum(), 44.901484)
bbox = QgsServerApiUtils.parseBbox('something_wrong_here')
self.assertTrue(bbox.isEmpty())
bbox = QgsServerApiUtils.parseBbox('8.203495,44.901482,8.203497,something_wrong_here')
self.assertTrue(bbox.isEmpty())
def test_published_crs(self):
"""Test published WMS CRSs"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
crss = QgsServerApiUtils.publishedCrsList(project)
self.assertTrue('http://www.opengis.net/def/crs/OGC/1.3/CRS84' in crss)
self.assertTrue('http://www.opengis.net/def/crs/EPSG/9.6.2/3857' in crss)
self.assertTrue('http://www.opengis.net/def/crs/EPSG/9.6.2/4326' in crss)
def test_parse_crs(self):
crs = QgsServerApiUtils.parseCrs('http://www.opengis.net/def/crs/OGC/1.3/CRS84')
self.assertTrue(crs.isValid())
self.assertEquals(crs.postgisSrid(), 4326)
crs = QgsServerApiUtils.parseCrs('http://www.opengis.net/def/crs/EPSG/9.6.2/3857')
self.assertTrue(crs.isValid())
self.assertEquals(crs.postgisSrid(), 3857)
crs = QgsServerApiUtils.parseCrs('http://www.opengis.net/something_wrong_here')
self.assertFalse(crs.isValid())
def test_append_path(self):
path = QgsServerApiUtils.appendMapParameter('/wfs3', QtCore.QUrl('https://www.qgis.org/wfs3?MAP=/some/path'))
self.assertEquals(path, '/wfs3?MAP=/some/path')
class API(QgsServerApi):
def __init__(self, iface, version='1.0'):
super().__init__(iface)
self._version = version
def name(self):
return "TEST"
def version(self):
return self._version
def rootPath(self):
return "/testapi"
def executeRequest(self, request_context):
request_context.response().write(b"\"Test API\"")
class QgsServerAPITestBase(QgsServerTestBase):
""" QGIS API server tests"""
# Set to True in child classes to re-generate reference files for this class
regeregenerate_api_reference = False
def dump(self, response):
"""Returns the response body as str"""
result = []
for n, v in response.headers().items():
if n == 'Content-Length':
continue
result.append("%s: %s" % (n, v))
result.append('')
result.append(bytes(response.body()).decode('utf8'))
return '\n'.join(result)
def compareApi(self, request, project, reference_file):
response = QgsBufferServerResponse()
# Add json to accept it reference_file is JSON
if reference_file.endswith('.json'):
request.setHeader('Accept', 'application/json')
self.server.handleRequest(request, response, project)
result = bytes(response.body()).decode('utf8') if reference_file.endswith('html') else self.dump(response)
path = unitTestDataPath('qgis_server') + '/api/' + reference_file
if self.regeregenerate_api_reference:
f = open(path.encode('utf8'), 'w+', encoding='utf8')
f.write(result)
f.close()
print("Reference file %s regenerated!" % path.encode('utf8'))
def __normalize_json(content):
reference_content = content.split('\n')
j = ''.join(reference_content[reference_content.index('') + 1:])
# Do not test timeStamp
j = json.loads(j)
try:
j['timeStamp'] = '2019-07-05T12:27:07Z'
except:
pass
json_content = json.dumps(j)
headers_content = '\n'.join(reference_content[:reference_content.index('') + 1])
return headers_content + '\n' + json_content
with open(path.encode('utf8'), 'r', encoding='utf8') as f:
if reference_file.endswith('json'):
self.assertEqual(__normalize_json(result), __normalize_json(f.read()))
else:
self.assertEqual(f.read(), result)
return response
def compareContentType(self, url, headers, content_type):
request = QgsBufferServerRequest(url, headers=headers)
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, QgsProject())
self.assertEqual(response.headers()['Content-Type'], content_type)
@classmethod
def setUpClass(cls):
super(QgsServerAPITestBase, cls).setUpClass()
cls.maxDiff = None
class QgsServerAPITest(QgsServerAPITestBase):
""" QGIS API server tests"""
def test_api(self):
"""Test API registering"""
api = API(self.server.serverInterface())
self.server.serverInterface().serviceRegistry().registerApi(api)
request = QgsBufferServerRequest('http://server.qgis.org/testapi')
self.compareApi(request, None, 'test_api.json')
self.server.serverInterface().serviceRegistry().unregisterApi(api.name())
def test_0_version_registration(self):
reg = QgsServiceRegistry()
api = API(self.server.serverInterface())
api1 = API(self.server.serverInterface(), '1.1')
# 1.1 comes first
reg.registerApi(api1)
reg.registerApi(api)
rapi = reg.getApi("TEST")
self.assertIsNotNone(rapi)
self.assertEqual(rapi.version(), "1.1")
rapi = reg.getApi("TEST", "2.0")
self.assertIsNotNone(rapi)
self.assertEqual(rapi.version(), "1.1")
rapi = reg.getApi("TEST", "1.0")
self.assertIsNotNone(rapi)
self.assertEqual(rapi.version(), "1.0")
def test_1_unregister_services(self):
reg = QgsServiceRegistry()
api = API(self.server.serverInterface(), '1.0a')
api1 = API(self.server.serverInterface(), '1.0b')
api2 = API(self.server.serverInterface(), '1.0c')
reg.registerApi(api)
reg.registerApi(api1)
reg.registerApi(api2)
# Check we get the default version
rapi = reg.getApi("TEST")
self.assertEqual(rapi.version(), "1.0a")
# Remove one service
removed = reg.unregisterApi("TEST", "1.0a")
self.assertEqual(removed, 1)
# Check that we get the highest version
rapi = reg.getApi("TEST")
self.assertEqual(rapi.version(), "1.0c")
# Remove all services
removed = reg.unregisterApi("TEST")
self.assertEqual(removed, 2)
# Check that there is no more services available
api = reg.getApi("TEST")
self.assertIsNone(api)
def test_wfs3_landing_page(self):
"""Test WFS3 API landing page in HTML format"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3.html')
self.compareApi(request, None, 'test_wfs3_landing_page.html')
def test_content_type_negotiation(self):
"""Test content-type negotiation and conflicts"""
# Default: json
self.compareContentType('http://server.qgis.org/wfs3', {}, 'application/json')
# Explicit request
self.compareContentType('http://server.qgis.org/wfs3', {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'}, 'text/html')
self.compareContentType('http://server.qgis.org/wfs3', {'Accept': 'application/json'}, 'application/json')
# File suffix
self.compareContentType('http://server.qgis.org/wfs3.json', {}, 'application/json')
self.compareContentType('http://server.qgis.org/wfs3.html', {}, 'text/html')
# File extension must take precedence over Accept header
self.compareContentType('http://server.qgis.org/wfs3.html', {'Accept': 'application/json'}, 'text/html')
self.compareContentType('http://server.qgis.org/wfs3.json', {'Accept': 'text/html'}, 'application/json')
def test_wfs3_landing_page_json(self):
"""Test WFS3 API landing page in JSON format"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3.json')
self.compareApi(request, None, 'test_wfs3_landing_page.json')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3')
request.setHeader('Accept', 'application/json')
self.compareApi(request, None, 'test_wfs3_landing_page.json')
def test_wfs3_api(self):
"""Test WFS3 API"""
# No project: error
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/api.openapi3')
self.compareApi(request, None, 'test_wfs3_api.json')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/api.openapi3')
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
self.compareApi(request, project, 'test_wfs3_api_project.json')
def test_wfs3_conformance(self):
"""Test WFS3 API"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/conformance')
self.compareApi(request, None, 'test_wfs3_conformance.json')
def test_wfs3_collections_empty(self):
"""Test WFS3 collections API"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections')
self.compareApi(request, None, 'test_wfs3_collections_empty.json')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections.json')
self.compareApi(request, None, 'test_wfs3_collections_empty.json')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections.html')
self.compareApi(request, None, 'test_wfs3_collections_empty.html')
def test_wfs3_collections_json(self):
"""Test WFS3 API collections in json format"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections.json')
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
self.compareApi(request, project, 'test_wfs3_collections_project.json')
def test_wfs3_collections_html(self):
"""Test WFS3 API collections in html format"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections.html')
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
self.compareApi(request, project, 'test_wfs3_collections_project.html')
def test_wfs3_collections_content_type(self):
"""Test WFS3 API collections in html format with Accept header"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections')
request.setHeader('Accept', 'text/html')
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(response.headers()['Content-Type'], 'text/html')
def test_wfs3_collection_items(self):
"""Test WFS3 API items"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé.json')
def test_wfs3_collection_items_crs(self):
"""Test WFS3 API items with CRS"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
encoded_crs = parse.quote('http://www.opengis.net/def/crs/EPSG/9.6.2/3857', safe='')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?crs={}'.format(encoded_crs))
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé_crs_3857.json')
def test_invalid_args(self):
"""Test wrong args"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(response.statusCode(), 400) # Bad request
self.assertEqual(response.body(), b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=10001')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(response.statusCode(), 400) # Bad request
self.assertEqual(response.body(), b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request
def test_wfs3_collection_items_limit(self):
"""Test WFS3 API item limits"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé_limit_1.json')
def test_wfs3_collection_items_limit_offset(self):
"""Test WFS3 API offset"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1&offset=1')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé_limit_1_offset_1.json')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=1&offset=-1')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(response.statusCode(), 400) # Bad request
self.assertEqual(response.body(), b'[{"code":"Bad request error","description":"Argument \'offset\' is not valid. Offset for features to retrieve [0-3]"}]') # Bad request
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1&offset=1')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, project)
self.assertEqual(response.statusCode(), 400) # Bad request
self.assertEqual(response.body(), b'[{"code":"Bad request error","description":"Argument \'limit\' is not valid. Number of features to retrieve [0-10000]"}]') # Bad request
def test_wfs3_collection_items_bbox(self):
"""Test WFS3 API bbox"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=8.203495,44.901482,8.203497,44.901484')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé_bbox.json')
# Test with a different CRS
encoded_crs = parse.quote('http://www.opengis.net/def/crs/EPSG/9.6.2/3857', safe='')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?bbox=913191,5606014,913234,5606029&bbox-crs={}'.format(encoded_crs))
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer_èé_bbox_3857.json')
def test_wfs3_static_handler(self):
"""Test static handler"""
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/static/style.css')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, None)
body = bytes(response.body()).decode('utf8')
self.assertTrue('Content-Length' in response.headers())
self.assertEqual(response.headers()['Content-Type'], 'text/css')
self.assertTrue(len(body) > 0)
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/static/does_not_exists.css')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response, None)
body = bytes(response.body()).decode('utf8')
self.assertEqual(body, '[{"code":"API not found error","description":"Static file does_not_exists.css was not found"}]')
def test_wfs3_field_filters(self):
"""Test field filters"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer3/items?name=two')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer3_name_eq_two.json')
def test_wfs3_field_filters_star(self):
"""Test field filters"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer3/items?name=tw*')
self.compareApi(request, project, 'test_wfs3_collections_items_testlayer3_name_eq_tw_star.json')
class Handler1(QgsServerOgcApiHandler):
def path(self):
return QtCore.QRegularExpression("/handlerone")
def operationId(self):
return "handlerOne"
def summary(self):
return "First of its name"
def description(self):
return "The first handler ever"
def linkTitle(self):
return "Handler One Link Title"
def linkType(self):
return QgsServerOgcApi.data
def handleRequest(self, context):
"""Simple mirror: returns the parameters"""
params = self.values(context)
self.write(params, context)
def parameters(self, context):
return [QgsServerQueryStringParameter('value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value')]
class Handler2(QgsServerOgcApiHandler):
def path(self):
return QtCore.QRegularExpression(r"/handlertwo/(?P<code1>\d{2})/(\d{3})")
def operationId(self):
return "handlerTwo"
def summary(self):
return "Second of its name"
def description(self):
return "The second handler ever"
def linkTitle(self):
return "Handler Two Link Title"
def linkType(self):
return QgsServerOgcApi.data
def handleRequest(self, context):
"""Simple mirror: returns the parameters"""
params = self.values(context)
self.write(params, context)
def parameters(self, context):
return [QgsServerQueryStringParameter('value1', True, QgsServerQueryStringParameter.Type.Double, 'a double value'),
QgsServerQueryStringParameter('value2', False, QgsServerQueryStringParameter.Type.String, 'a string value'), ]
class QgsServerOgcAPITest(QgsServerAPITestBase):
""" QGIS OGC API server tests"""
def testOgcApi(self):
"""Test OGC API"""
api = QgsServerOgcApi(self.server.serverInterface(), '/api1', 'apione', 'an api', '1.1')
self.assertEqual(api.name(), 'apione')
self.assertEqual(api.description(), 'an api')
self.assertEqual(api.version(), '1.1')
self.assertEqual(api.rootPath(), '/api1')
url = 'http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1'
self.assertEqual(api.sanitizeUrl(QtCore.QUrl(url)).toString(), 'http://server.qgis.org/wfs3/collections/testlayer \xe8\xe9/items?limit=-1')
self.assertEqual(api.sanitizeUrl(QtCore.QUrl('/path//double//slashes//#fr')).toString(), '/path/double/slashes#fr')
self.assertEqual(api.relToString(QgsServerOgcApi.data), 'data')
self.assertEqual(api.relToString(QgsServerOgcApi.alternate), 'alternate')
self.assertEqual(api.contentTypeToString(QgsServerOgcApi.JSON), 'JSON')
self.assertEqual(api.contentTypeToStdString(QgsServerOgcApi.JSON), 'JSON')
self.assertEqual(api.contentTypeToExtension(QgsServerOgcApi.JSON), 'json')
self.assertEqual(api.contentTypeToExtension(QgsServerOgcApi.GEOJSON), 'geojson')
def testOgcApiHandler(self):
"""Test OGC API Handler"""
project = QgsProject()
project.read(unitTestDataPath('qgis_server') + '/test_project.qgs')
request = QgsBufferServerRequest('http://server.qgis.org/wfs3/collections/testlayer%20èé/items?limit=-1')
response = QgsBufferServerResponse()
ctx = QgsServerApiContext('/services/api1', request, response, project, self.server.serverInterface())
h = Handler1()
self.assertTrue(h.staticPath(ctx).endswith('/resources/server/api/ogc/static'))
self.assertEqual(h.path(), QtCore.QRegularExpression("/handlerone"))
self.assertEqual(h.description(), 'The first handler ever')
self.assertEqual(h.operationId(), 'handlerOne')
self.assertEqual(h.summary(), 'First of its name')
self.assertEqual(h.linkTitle(), 'Handler One Link Title')
self.assertEqual(h.linkType(), QgsServerOgcApi.data)
with self.assertRaises(QgsServerApiBadRequestException) as ex:
h.handleRequest(ctx)
self.assertEqual(str(ex.exception), 'Missing required argument: \'value1\'')
r = ctx.response()
self.assertEqual(r.data(), '')
with self.assertRaises(QgsServerApiBadRequestException) as ex:
h.values(ctx)
self.assertEqual(str(ex.exception), 'Missing required argument: \'value1\'')
# Add handler to API and test for /api2
ctx = QgsServerApiContext('/services/api2', request, response, project, self.server.serverInterface())
api = QgsServerOgcApi(self.server.serverInterface(), '/api2', 'apitwo', 'a second api', '1.2')
api.registerHandler(h)
# Add a second handler (will be tested later)
h2 = Handler2()
api.registerHandler(h2)
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api1'))
with self.assertRaises(QgsServerApiBadRequestException) as ex:
api.executeRequest(ctx)
self.assertEqual(str(ex.exception), 'Requested URI does not match any registered API handler')
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2'))
with self.assertRaises(QgsServerApiBadRequestException) as ex:
api.executeRequest(ctx)
self.assertEqual(str(ex.exception), 'Requested URI does not match any registered API handler')
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlerone'))
with self.assertRaises(QgsServerApiBadRequestException) as ex:
api.executeRequest(ctx)
self.assertEqual(str(ex.exception), 'Missing required argument: \'value1\'')
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlerone?value1=not+a+double'))
with self.assertRaises(QgsServerApiBadRequestException) as ex:
api.executeRequest(ctx)
self.assertEqual(str(ex.exception), 'Argument \'value1\' could not be converted to Double')
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlerone?value1=1.2345'))
params = h.values(ctx)
self.assertEqual(params, {'value1': 1.2345})
api.executeRequest(ctx)
self.assertEqual(json.loads(bytes(ctx.response().data()))['value1'], 1.2345)
# Test path fragments extraction
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345'))
params = h2.values(ctx)
self.assertEqual(params, {'code1': '00', 'value1': 1.2345, 'value2': None})
# Test string encoding
ctx.request().setUrl(QtCore.QUrl('http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345&value2=a%2Fstring%20some'))
params = h2.values(ctx)
self.assertEqual(params, {'code1': '00', 'value1': 1.2345, 'value2': 'a/string some'})
# Test links
self.assertEqual(h2.href(ctx), 'http://www.qgis.org/services/api2/handlertwo/00/555?value1=1.2345&value2=a%2Fstring%20some')
self.assertEqual(h2.href(ctx, '/extra'), 'http://www.qgis.org/services/api2/handlertwo/00/555/extra?value1=1.2345&value2=a%2Fstring%20some')
self.assertEqual(h2.href(ctx, '/extra', 'json'), 'http://www.qgis.org/services/api2/handlertwo/00/555/extra.json?value1=1.2345&value2=a%2Fstring%20some')
# Test template path
self.assertTrue(h2.templatePath(ctx).endswith('/resources/server/api/ogc/templates/services/api2/handlerTwo.html'))
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsServerApiContext class.
.. 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__ = 'Alessandro Pasotti'
__date__ = '11/07/2019'
__copyright__ = 'Copyright 2019, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import os
import json
import re
# Deterministic XML
os.environ['QT_HASH_SEED'] = '1'
from qgis.server import (
QgsBufferServerRequest,
QgsBufferServerResponse,
QgsServerApiContext
)
from qgis.testing import unittest
from utilities import unitTestDataPath
from urllib import parse
import tempfile
from test_qgsserver import QgsServerTestBase
class QgsServerApiContextsTest(QgsServerTestBase):
""" QGIS Server API context tests"""
def testMatchedPath(self):
"""Test path extraction"""
response = QgsBufferServerResponse()
request = QgsBufferServerRequest("http://www.qgis.org/services/wfs3")
context = QgsServerApiContext("/wfs3", request, response, None, None)
self.assertEqual(context.matchedPath(), "/services/wfs3")
request = QgsBufferServerRequest("http://www.qgis.org/services/wfs3/collections.hml")
context = QgsServerApiContext("/wfs3", request, response, None, None)
self.assertEqual(context.matchedPath(), "/services/wfs3")
request = QgsBufferServerRequest("http://www.qgis.org/services/wfs3/collections.hml")
context = QgsServerApiContext("/wfs4", request, response, None, None)
self.assertEqual(context.matchedPath(), "")
if __name__ == '__main__':
unittest.main()

View File

@ -124,11 +124,11 @@ class TestServices(unittest.TestCase):
def test_0_version_registration(self):
reg = QgsServiceRegistry()
myserv1 = MyService("TEST", "1.1", "Hello")
myserv2 = MyService("TEST", "1.0", "Hello")
myserv11 = MyService("TEST", "1.1", "Hello")
myserv10 = MyService("TEST", "1.0", "Hello")
reg.registerService(myserv1)
reg.registerService(myserv2)
reg.registerService(myserv11)
reg.registerService(myserv10)
service = reg.getService("TEST")
self.assertIsNotNone(service)

View File

@ -1 +1,47 @@
ADD_SUBDIRECTORY(wms)
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/external
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/server
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/geometry
${CMAKE_SOURCE_DIR}/src/server
${CMAKE_SOURCE_DIR}/src/test
)
#note for tests we should not include the moc of our
#qtests in the executable file list as the moc is
#directly included in the sources
#and should not be compiled twice. Trying to include
#them in will cause an error at build time
#No relinking and full RPATH for the install tree
#See: http://www.cmake.org/Wiki/CMake_RPATH_handling#No_relinking_and_full_RPATH_for_the_install_tree
MACRO (ADD_QGIS_TEST TESTSRC)
SET (TESTNAME ${TESTSRC})
STRING(REPLACE "test" "" TESTNAME ${TESTNAME})
STRING(REPLACE "qgs" "" TESTNAME ${TESTNAME})
STRING(REPLACE ".cpp" "" TESTNAME ${TESTNAME})
SET (TESTNAME "qgis_${TESTNAME}test")
ADD_EXECUTABLE(${TESTNAME} ${TESTSRC} ${util_SRCS})
SET_TARGET_PROPERTIES(${TESTNAME} PROPERTIES AUTOMOC TRUE)
TARGET_LINK_LIBRARIES(${TESTNAME}
${Qt5Core_LIBRARIES}
${Qt5Test_LIBRARIES}
qgis_server)
ADD_TEST(${TESTNAME} ${CMAKE_BINARY_DIR}/output/bin/${TESTNAME} -maxwarnings 10000)
ENDMACRO (ADD_QGIS_TEST)
#############################################################
# Tests:
SET(TESTS
testqgsserverquerystringparameter.cpp
)
FOREACH(TESTSRC ${TESTS})
ADD_QGIS_TEST(${TESTSRC})
ENDFOREACH(TESTSRC)

View File

@ -0,0 +1,177 @@
/***************************************************************************
testqgsserverquerystringparameter.cpp
--------------------------------------
Date : Jul 10 2019
Copyright : (C) 2019 by Alessandro Pasotti
Email : elpaso at itopen dot it
***************************************************************************
* *
* 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 "qgstest.h"
#include <QObject>
#include <QString>
#include <QStringList>
//qgis includes...
#include "qgsserverquerystringparameter.h"
#include "qgsserverapicontext.h"
#include "qgsserverrequest.h"
#include "qgsserverexception.h"
/**
* \ingroup UnitTests
* Unit tests for the server query string parameter
*/
class TestQgsServerQueryStringParameter : public QObject
{
Q_OBJECT
public:
TestQgsServerQueryStringParameter() = default;
private slots:
// will be called before the first testfunction is executed.
void initTestCase();
// will be called after the last testfunction was executed.
void cleanupTestCase();
// will be called before each testfunction is executed
void init();
// will be called after every testfunction.
void cleanup();
// Basic test on types and constraints
void testArguments();
// Test custom validators
void testCustomValidators();
// Test default values
void testDefaultValues();
};
void TestQgsServerQueryStringParameter::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
QgsApplication::showSettings();
}
void TestQgsServerQueryStringParameter::cleanupTestCase()
{
QgsApplication::exitQgis();
}
void TestQgsServerQueryStringParameter::init()
{
}
void TestQgsServerQueryStringParameter::cleanup()
{
}
void TestQgsServerQueryStringParameter::testArguments()
{
QgsServerQueryStringParameter p { QStringLiteral( "parameter1" ) };
QgsServerRequest request;
QgsServerApiContext ctx { "/wfs3", &request, nullptr, nullptr, nullptr };
// Test string (default)
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123" ) );
QCOMPARE( p.value( ctx ).toString(), QString( "123" ) );
QCOMPARE( p.value( ctx ).type(), QVariant::String );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=a%20string" ) );
QCOMPARE( p.value( ctx ).toString(), QString( "a string" ) );
QCOMPARE( p.value( ctx ).type(), QVariant::String );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/" ) );
QCOMPARE( p.value( ctx ).toString(), QString() );
// Test required
p.mRequired = true;
request.setUrl( QStringLiteral( "http://www.qgis.org/api/" ) );
QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException );
// Test int
p.mType = QgsServerQueryStringParameter::Type::Integer;
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123" ) );
QCOMPARE( p.value( ctx ).toInt(), 123 );
QCOMPARE( p.value( ctx ).type(), QVariant::LongLong );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=a%20string" ) );
QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException );
// Test double
p.mType = QgsServerQueryStringParameter::Type::Double;
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123" ) );
QCOMPARE( p.value( ctx ).toDouble(), 123.0 );
QCOMPARE( p.value( ctx ).type(), QVariant::Double );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123.456" ) );
QCOMPARE( p.value( ctx ).toDouble(), 123.456 );
QCOMPARE( p.value( ctx ).type(), QVariant::Double );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=a%20string" ) );
QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException );
// Test list
p.mType = QgsServerQueryStringParameter::Type::List;
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123,a%20value" ) );
QCOMPARE( p.value( ctx ).toStringList(), QStringList() << QStringLiteral( "123" ) << QStringLiteral( "a value" ) );
QCOMPARE( p.value( ctx ).type(), QVariant::StringList );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=a%20value" ) );
QCOMPARE( p.value( ctx ).toStringList(), QStringList() << QStringLiteral( "a value" ) );
QCOMPARE( p.value( ctx ).type(), QVariant::StringList );
}
void TestQgsServerQueryStringParameter::testCustomValidators()
{
QgsServerQueryStringParameter p { QStringLiteral( "parameter1" ), true, QgsServerQueryStringParameter::Type::Integer };
QgsServerRequest request;
QgsServerApiContext ctx { "/wfs3", &request, nullptr, nullptr, nullptr };
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=123" ) );
QCOMPARE( p.value( ctx ).toInt(), 123 );
// Test a range validator that increments the value
QgsServerQueryStringParameter::customValidator validator = [ ]( const QgsServerApiContext &, QVariant & value ) -> bool
{
const auto v { value.toLongLong() };
// Change the value by adding 1
value.setValue( v + 1 );
return v > 500 && v < 1000;
};
p.setCustomValidator( validator );
QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=501" ) );
QCOMPARE( p.value( ctx ).toInt(), 502 );
QCOMPARE( p.value( ctx ).type(), QVariant::LongLong );
}
void TestQgsServerQueryStringParameter::testDefaultValues()
{
// Set a default AND required, verify it's ignored
QgsServerQueryStringParameter p { QStringLiteral( "parameter1" ), true, QgsServerQueryStringParameter::Type::Integer, QStringLiteral( "Paramerer 1" ), 10 };
QgsServerRequest request;
QgsServerApiContext ctx { "/wfs3", &request, nullptr, nullptr, nullptr };
request.setUrl( QStringLiteral( "http://www.qgis.org/api/" ) );
QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException );
QgsServerQueryStringParameter p2 { QStringLiteral( "parameter1" ), false, QgsServerQueryStringParameter::Type::Integer, QStringLiteral( "Paramerer 1" ), 10 };
QCOMPARE( p2.value( ctx ).toInt(), 10 );
request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=501" ) );
QCOMPARE( p2.value( ctx ).toInt(), 501 );
}
QGSTEST_MAIN( TestQgsServerQueryStringParameter )
#include "testqgsserverquerystringparameter.moc"

View File

@ -2,6 +2,7 @@
# Don't forget to include output directory, otherwise
# the UI file won't be wrapped!
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/external
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/geometry

View File

@ -0,0 +1,2 @@
"Test API"

View File

@ -0,0 +1,3 @@
Content-Type: application/json
[{"code":"Improperly configured error","description":"Project not found, please check your server configuration."}]

View File

@ -0,0 +1,881 @@
Content-Type: application/openapi+json;version=3.0
{
"components": {
"parameters": {
"bbox": {
"description": "Only features that have a geometry that intersects the bounding box are selected. The bounding box is provided as four or six numbers, depending on whether the coordinate reference system includes a vertical axis (elevation or depth):\n \n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Lower left corner, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Upper right corner, coordinate axis 3 (optional)\n\nThe coordinate reference system of the values is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nFor WGS84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge).\n\nIf a feature has multiple spatial geometry properties, it is the decision of the server whether only a single spatial geometry property is used to determine the extent or all relevant geometries.",
"explode": false,
"in": "query",
"name": "bbox",
"required": false,
"schema": {
"items": {
"type": "number"
},
"maxItems": 6,
"minItems": 4,
"type": "array"
},
"style": "form"
},
"bbox-crs": {
"description": "The coordinate reference system of the bbox parameter. Default is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84).",
"explode": false,
"in": "query",
"name": "bbox-crs",
"required": false,
"schema": {
"default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"enum": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"http://www.opengis.net/def/crs/EPSG/9.6.2/4326",
"http://www.opengis.net/def/crs/EPSG/9.6.2/3857"
],
"type": "string"
},
"style": "form"
},
"crs": {
"description": "The coordinate reference system of the response geometries. Default is WGS84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84).",
"explode": false,
"in": "query",
"name": "crs",
"required": false,
"schema": {
"default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"enum": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"http://www.opengis.net/def/crs/EPSG/9.6.2/4326",
"http://www.opengis.net/def/crs/EPSG/9.6.2/3857"
],
"type": "string"
},
"style": "form"
},
"featureId": {
"description": "Local identifier of a specific feature",
"in": "path",
"name": "featureId",
"required": true,
"schema": {
"type": "string"
}
},
"limit": {
"description": "The optional limit parameter limits the number of items that are presented in the response document.\\\nOnly items are counted that are on the first level of the collection in the response document. Nested objects contained within the explicitly requested items shall not be counted.\\\nMinimum = 1.\\\nMaximum = 10000.\\\nDefault = 10.",
"example": 10,
"explode": false,
"in": "query",
"name": "limit",
"required": false,
"schema": {
"default": 10,
"maximum": 10000,
"minimum": 1,
"type": "integer"
},
"style": "form"
},
"offset": {
"description": "The optional offset parameter indicates the index within the result set from which the server shall begin presenting results in the response document. The first element has an index of 0.\\\nMinimum = 0.\\\nDefault = 0.",
"example": 0,
"explode": false,
"in": "query",
"name": "offset",
"required": false,
"schema": {
"default": 0,
"minimum": 0,
"type": "integer"
},
"style": "form"
},
"relations": {
"description": "Comma-separated list of related collections that should be shown for this feature",
"explode": false,
"in": "query",
"name": "relations",
"required": false,
"schema": {
"items": {
"type": "string"
},
"type": "array"
},
"style": "form"
},
"resultType": {
"description": "This service will respond to a query in one of two ways (excluding an exception response). It may either generate a complete response document containing resources that satisfy the operation or it may simply generate an empty response container that indicates the count of the total number of resources that the operation would return. Which of these two responses is generated is determined by the value of the optional resultType parameter.\\\nThe allowed values for this parameter are \"results\" and \"hits\".\\\nIf the value of the resultType parameter is set to \"results\", the server will generate a complete response document containing resources that satisfy the operation.\\\nIf the value of the resultType attribute is set to \"hits\", the server will generate an empty response document containing no resource instances.\\\nDefault = \"results\".",
"example": "results",
"explode": false,
"in": "query",
"name": "resultType",
"required": false,
"schema": {
"default": "results",
"enum": [
"hits",
"results"
],
"type": "string"
},
"style": "form"
},
"time": {
"description": "Either a date-time or a period string that adheres to RFC 3339. Examples:\n\n* A date-time: \"2018-02-12T23:20:50Z\"\n* A period: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\" or \"2018-02-12T00:00:00Z/P1M6DT12H31M12S\"\n\nOnly features that have a temporal property that intersects the value of\n`time` are selected.\n\nIf a feature has multiple temporal properties, it is the decision of the\nserver whether only a single temporal property is used to determine\nthe extent or all relevant temporal properties.",
"explode": false,
"in": "query",
"name": "time",
"required": false,
"schema": {
"type": "string"
},
"style": "form"
}
},
"schemas": {
"collectionInfo": {
"properties": {
"crs": {
"default": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84"
],
"description": "The coordinate reference systems in which geometries may be retrieved. Coordinate reference systems are identified by a URI. The first coordinate reference system is the coordinate reference system that is used by default. This is always \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", i.e. WGS84 longitude/latitude.",
"example": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"http://www.opengis.net/def/crs/EPSG/0/4326"
],
"items": {
"type": "string"
},
"type": "array"
},
"description": {
"description": "a description of the features in the collection",
"example": "Buildings in the city of Bonn.",
"type": "string"
},
"extent": {
"$ref": "#/components/schemas/extent"
},
"links": {
"example": [
{
"href": "http://data.example.org/collections/buildings/items",
"rel": "item",
"title": "Buildings",
"type": "application/geo+json"
},
{
"href": "http://example.com/concepts/buildings.html",
"rel": "describedBy",
"title": "Feature catalogue for buildings",
"type": "text/html"
}
],
"items": {
"$ref": "#/components/schemas/link"
},
"type": "array"
},
"name": {
"description": "identifier of the collection used, for example, in URIs",
"example": "buildings",
"type": "string"
},
"relations": {
"description": "Related collections that may be retrieved for this collection",
"example": "{\"id\": \"label\"}",
"type": "object"
},
"title": {
"description": "human readable title of the collection",
"example": "Buildings",
"type": "string"
}
},
"required": [
"links",
"name"
],
"type": "object"
},
"content": {
"properties": {
"collections": {
"items": {
"$ref": "#/components/schemas/collectionInfo"
},
"type": "array"
},
"links": {
"example": [
{
"href": "http://data.example.org/collections.json",
"rel": "self",
"title": "this document",
"type": "application/json"
},
{
"href": "http://data.example.org/collections.html",
"rel": "alternate",
"title": "this document as HTML",
"type": "text/html"
},
{
"href": "http://schemas.example.org/1.0/foobar.xsd",
"rel": "describedBy",
"title": "XML schema for Acme Corporation data",
"type": "application/xml"
}
],
"items": {
"$ref": "#/components/schemas/link"
},
"type": "array"
}
},
"required": [
"collections",
"links"
],
"type": "object"
},
"exception": {
"properties": {
"code": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"code"
],
"type": "object"
},
"extent": {
"properties": {
"crs": {
"default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
"description": "Coordinate reference system of the coordinates in the spatial extent (property `spatial`). In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional coordinate reference systems.",
"enum": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84"
],
"type": "string"
},
"spatial": {
"description": "West, north, east, south edges of the spatial extent. The minimum and maximum values apply to the coordinate reference system WGS84 longitude/latitude that is supported in the Core. If, for example, a projected coordinate reference system is used, the minimum and maximum values need to be adjusted.",
"example": [
-180,
-90,
180,
90
],
"items": {
"type": "number"
},
"maxItems": 6,
"minItems": 4,
"type": "array"
}
},
"required": [
"spatial"
],
"type": "object"
},
"featureCollectionGeoJSON": {
"properties": {
"features": {
"items": {
"$ref": "#/components/schemas/featureGeoJSON"
},
"type": "array"
},
"links": {
"items": {
"$ref": "#/components/schemas/link"
},
"type": "array"
},
"numberMatched": {
"minimum": 0,
"type": "integer"
},
"numberReturned": {
"minimum": 0,
"type": "integer"
},
"timeStamp": {
"format": "dateTime",
"type": "string"
},
"type": {
"enum": [
"FeatureCollection"
],
"type": "string"
}
},
"required": [
"features",
"type"
],
"type": "object"
},
"featureGeoJSON": {
"properties": {
"geometry": {
"$ref": "#/components/schemas/geometryGeoJSON"
},
"id": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"properties": {
"nullable": true,
"type": "object"
},
"type": {
"enum": [
"Feature"
],
"type": "string"
}
},
"required": [
"geometry",
"properties",
"type"
],
"type": "object"
},
"geometryGeoJSON": {
"properties": {
"type": {
"enum": [
"Point",
"MultiPoint",
"LineString",
"MultiLineString",
"Polygon",
"MultiPolygon",
"GeometryCollection"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"link": {
"properties": {
"href": {
"example": "http://data.example.com/buildings/123",
"type": "string"
},
"hreflang": {
"example": "en",
"type": "string"
},
"rel": {
"example": "prev",
"type": "string"
},
"type": {
"example": "application/geo+json",
"type": "string"
}
},
"required": [
"href"
],
"type": "object"
},
"req-classes": {
"properties": {
"conformsTo": {
"example": [
"http://www.opengis.net/spec/wfs-1/3.0/req/core",
"http://www.opengis.net/spec/wfs-1/3.0/req/oas30",
"http://www.opengis.net/spec/wfs-1/3.0/req/html",
"http://www.opengis.net/spec/wfs-1/3.0/req/geojson"
],
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"conformsTo"
],
"type": "object"
},
"root": {
"properties": {
"links": {
"example": [
{
"href": "http://data.example.org/",
"rel": "self",
"title": "this document",
"type": "application/json"
},
{
"href": "http://data.example.org/api",
"rel": "service",
"title": "the API definition",
"type": "application/openapi+json;version=3.0"
},
{
"href": "http://data.example.org/conformance",
"rel": "conformance",
"title": "WFS 3.0 conformance classes implemented by this server",
"type": "application/json"
},
{
"href": "http://data.example.org/collections",
"rel": "data",
"title": "Metadata about the feature collections",
"type": "application/json"
}
],
"items": {
"$ref": "#/components/schemas/link"
},
"type": "array"
}
},
"required": [
"links"
],
"type": "object"
}
}
},
"info": {
"contact": {
"email": "elpaso@itopen.it",
"name": "Alessandro Pasotti",
"url": ""
},
"description": "Some UTF8 text èòù",
"license": {
"name": ""
},
"title": "QGIS TestProject",
"version": ""
},
"links": [
{
"href": "http://server.qgis.org/wfs3/api.openapi3",
"rel": "self",
"title": "API definition as OPENAPI3",
"type": "application/openapi+json;version=3.0"
},
{
"href": "http://server.qgis.org/wfs3/api.html",
"rel": "alternate",
"title": "API definition as HTML",
"type": "text/html"
}
],
"openapi": "3.0.1",
"paths": {
"/wfs3": {
"get": {
"description": "The landing page provides links to the API definition, the Conformance statements and the metadata about the feature data in this dataset.",
"operationId": "getLandingPage",
"responses": [
[
"200",
{
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/root"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "The landing page provides links to the API definition, the Conformance statements and the metadata about the feature data in this dataset."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "WFS 3.0 Landing Page",
"tags": "Capabilities"
}
},
"/wfs3/collections": {
"get": {
"description": "Describe the feature collections in the dataset statements and the metadata about the feature data in this dataset.",
"operationId": "describeCollections",
"responses": [
[
"200",
{
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/content"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "Describe the feature collections in the dataset statements and the metadata about the feature data in this dataset."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "Metadata about the feature collections shared by this API.",
"tags": "Capabilities"
}
},
"/wfs3/collections/testlayer èé/items": {
"get": {
"description": "Every feature in a dataset belongs to a collection. A dataset may consist of multiple feature collections. A feature collection is often a collection of features of a similar type, based on a common schema. Use content negotiation or specify a file extension to request HTML (.html) or GeoJSON (.json).",
"operationId": "getFeatures_testlayer èé",
"parameters": [
[
{
"$ref": "#/components/parameters/limit"
},
{
"$ref": "#/components/parameters/offset"
},
{
"$ref": "#/components/parameters/resultType"
},
{
"$ref": "#/components/parameters/bbox"
},
{
"$ref": "#/components/parameters/bbox-crs"
}
],
{
"description": "Filter the collection by 'id'",
"explode": false,
"in": "query",
"name": "id",
"required": false,
"schema": {
"type": "integer"
},
"style": "form"
},
{
"description": "Filter the collection by 'name'",
"explode": false,
"in": "query",
"name": "name",
"required": false,
"schema": {
"type": "string"
},
"style": "form"
},
{
"description": "Filter the collection by 'utf8nameè'",
"explode": false,
"in": "query",
"name": "utf8nameè",
"required": false,
"schema": {
"type": "string"
},
"style": "form"
}
],
"responses": [
[
"200",
{
"content": {
"application/geo+json": {
"schema": {
"$ref": "#/components/schemas/featureCollectionGeoJSON"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "Metadata about the collection 'A test vector layer' shared by this API."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "Retrieve features of 'A test vector layer' feature collection",
"tags": "Features"
}
},
"/wfs3/conformance": {
"get": {
"description": "List all requirements classes specified in a standard (e.g., WFS 3.0 Part 1: Core) that the server conforms to",
"operationId": "getRequirementClasses",
"responses": [
[
"200",
{
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/root"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "List all requirements classes specified in a standard (e.g., WFS 3.0 Part 1: Core) that the server conforms to"
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "Information about standards that this API conforms to",
"tags": "Capabilities"
}
},
"/wfs3api": {
"get": {
"description": "The formal documentation of this API according to the OpenAPI specification, version 3.0. I.e., this document.",
"operationId": "getApiDescription",
"responses": [
[
"200",
{
"content": {
"application/openapi+json;version=3.0": {
"schema": {
"type": "object"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "The formal documentation of this API according to the OpenAPI specification, version 3.0. I.e., this document."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "The API definition",
"tags": "Capabilities"
}
},
"/wfs3collections/testlayer èé": {
"get": {
"description": "Metadata about a feature collection.",
"operationId": "describeCollection_testlayer èé",
"responses": [
[
"200",
{
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/collectionInfo"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "Metadata about the collection 'A test vector layer' shared by this API."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "Describe the 'A test vector layer' feature collection",
"tags": "Capabilities"
}
},
"/wfs3collections/testlayer èé/items/{featureId}": {
"get": {
"description": "Retrieve a feature; use content negotiation or specify a file extension to request HTML (.html or GeoJSON (.json)",
"operationId": "getFeature_testlayer èé",
"responses": [
[
"200",
{
"content": {
"application/geo+json": {
"schema": {
"$ref": "#/components/schemas/featureGeoJSON"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
},
"description": "Retrieve a 'A test vector layer' feature by 'featureId'."
}
],
{
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/exception"
},
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"description": "An error occurred."
}
}
],
"summary": "Retrieve a single feature from the 'A test vector layer' feature collection",
"tags": "Features"
}
}
},
"servers": [
{
"url": "http://server.qgis.org/wfs3"
}
],
"tags": [
{
"description": "Essential characteristics of this API including information about the data.",
"name": "Capabilities"
},
{
"description": "Access to data (features).",
"name": "Features"
}
],
"timeStamp": "2019-07-30T09:17:49Z"
}

View File

@ -0,0 +1,76 @@
<!-- template for the WFS3 API collections page -->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/>
<link rel="stylesheet" href="/wfs3/static/style.css" />
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="/wfs3/static/jsonFormatter.min.css" crossorigin="anonymous"></script>
<script src="/wfs3/static/jsonFormatter.min.js" crossorigin="anonymous"></script>
<title>Feature collections</title>
</head>
<body>
<nav class="navbar navbar-light bg-light navbar-expand-sm">
<div class="container">
<div id="navbar" class="navbar-collapse collapse d-flex justify-content-between align-items-center">
<ol class="breadcrumb bg-light my-0 pl-0">
<li class="breadcrumb-item"><a href="http://server.qgis.org/wfs3" >Landing page</a></li>
<li class="breadcrumb-item active">
Feature collections
</li>
</ol>
<ul class="list-unstyled list-separated m-0 p-0 text-muted">
<li><a rel="alternate" href="http://server.qgis.org/wfs3/collections.json" target="_blank">JSON</a></li>
</ul>
</div>
</div>
</nav>
<div class="container pt-4">
<!-- END HEADER TEMPLATE header.html -->
<h1>Collections</h1>
<!-- FOOTER TEMPLATE footer.html -->
</div> <!-- //container -->
<footer class="footer bg-light py-4 d-flex flex-column justify-content-around align-items-center">
<div class="container d-flex flex-row justify-content-between align-items-center w-100">
<span><span class="text-muted small mr-2">powered by</span><a class="navbar-brand" href="https://www.qgis.org/" target="_blank">QGIS Server</a></span>
</div>
</footer>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"
integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og=="
crossorigin=""></script>
<script>
jQuery('.jref').jsonFormatter();
</script>
</body>
</html>

View File

@ -0,0 +1,23 @@
Content-Type: application/json
{
"collections": [],
"crs": [
"http://www.opengis.net/def/crs/OGC/1.3/CRS84"
],
"links": [
{
"href": "http://server.qgis.org/wfs3/collections.json",
"rel": "self",
"title": "Feature collections as JSON",
"type": "application/json"
},
{
"href": "http://server.qgis.org/wfs3/collections.html",
"rel": "alternate",
"title": "Feature collections as HTML",
"type": "text/html"
}
],
"timeStamp": "2019-07-30T09:17:49Z"
}

Some files were not shown because too many files have changed in this diff Show More