mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-13 00:03:09 -04:00
[server] Server api and WFS3 (#10016)
Implementation of OGC API and WFS3 core draft specification
This commit is contained in:
parent
8d44f84cd0
commit
92ac7a2e93
3345
external/inja/inja.hpp
vendored
Normal file
3345
external/inja/inja.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
@ -250,6 +250,7 @@ Returns a GeoJSON string representation of a list of features (feature collectio
|
||||
.. seealso:: :py:func:`exportFeature`
|
||||
%End
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
42
python/core/std.sip
Normal 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
|
||||
};
|
3
python/server/auto_additions/qgsserverogcapi.py
Normal file
3
python/server/auto_additions/qgsserverogcapi.py
Normal file
@ -0,0 +1,3 @@
|
||||
# The following has been generated automatically from src/server/qgsserverogcapi.h
|
||||
QgsServerOgcApi.Rel.baseClass = QgsServerOgcApi
|
||||
QgsServerOgcApi.ContentType.baseClass = QgsServerOgcApi
|
@ -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
|
@ -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:
|
||||
|
130
python/server/auto_generated/qgsserverapi.sip.in
Normal file
130
python/server/auto_generated/qgsserverapi.sip.in
Normal 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 *
|
||||
************************************************************************/
|
100
python/server/auto_generated/qgsserverapicontext.sip.in
Normal file
100
python/server/auto_generated/qgsserverapicontext.sip.in
Normal 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 *
|
||||
************************************************************************/
|
94
python/server/auto_generated/qgsserverapiutils.sip.in
Normal file
94
python/server/auto_generated/qgsserverapiutils.sip.in
Normal 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 *
|
||||
************************************************************************/
|
@ -11,6 +11,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class QgsServerException
|
||||
{
|
||||
%Docstring
|
||||
@ -94,6 +96,7 @@ Returns the exception version
|
||||
};
|
||||
|
||||
|
||||
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
|
140
python/server/auto_generated/qgsserverogcapi.sip.in
Normal file
140
python/server/auto_generated/qgsserverogcapi.sip.in
Normal 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 *
|
||||
************************************************************************/
|
229
python/server/auto_generated/qgsserverogcapihandler.sip.in
Normal file
229
python/server/auto_generated/qgsserverogcapihandler.sip.in
Normal 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 *
|
||||
************************************************************************/
|
@ -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 *
|
||||
************************************************************************/
|
@ -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:
|
||||
|
@ -98,6 +98,7 @@ to the underlying I/O device
|
||||
|
||||
|
||||
|
||||
|
||||
virtual void write( const QgsServerException &ex );
|
||||
%Docstring
|
||||
Write server exception
|
||||
|
@ -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
|
||||
|
||||
};
|
||||
|
@ -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
|
||||
|
23
python/server/qgsserverexception.sip
Normal file
23
python/server/qgsserverexception.sip
Normal 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
|
||||
};
|
@ -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
|
||||
|
@ -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
|
||||
|
385
resources/server/api/ogc/schema.json
Normal file
385
resources/server/api/ogc/schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
resources/server/api/ogc/static/jsonFormatter.min.css
vendored
Normal file
1
resources/server/api/ogc/static/jsonFormatter.min.css
vendored
Normal 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}
|
2
resources/server/api/ogc/static/jsonFormatter.min.js
vendored
Normal file
2
resources/server/api/ogc/static/jsonFormatter.min.js
vendored
Normal 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("<").split(">").join(">"));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
|
11
resources/server/api/ogc/static/style.css
Normal file
11
resources/server/api/ogc/static/style.css
Normal file
@ -0,0 +1,11 @@
|
||||
a { color: green; }
|
||||
|
||||
#mapid.small {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
|
||||
.card-header span.small {
|
||||
font-size: 70%;
|
||||
}
|
@ -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: '© <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" %}
|
@ -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" %}
|
22
resources/server/api/ogc/templates/wfs3/footer.html
Normal file
22
resources/server/api/ogc/templates/wfs3/footer.html
Normal 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>
|
170
resources/server/api/ogc/templates/wfs3/getApiDescription.html
Normal file
170
resources/server/api/ogc/templates/wfs3/getApiDescription.html
Normal 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" %}
|
20
resources/server/api/ogc/templates/wfs3/getFeature.html
Normal file
20
resources/server/api/ogc/templates/wfs3/getFeature.html
Normal 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" %}
|
39
resources/server/api/ogc/templates/wfs3/getFeatures.html
Normal file
39
resources/server/api/ogc/templates/wfs3/getFeatures.html
Normal 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" %}
|
17
resources/server/api/ogc/templates/wfs3/getLandingPage.html
Normal file
17
resources/server/api/ogc/templates/wfs3/getLandingPage.html
Normal 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" %}
|
@ -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" %}
|
45
resources/server/api/ogc/templates/wfs3/header.html
Normal file
45
resources/server/api/ogc/templates/wfs3/header.html
Normal 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 -->
|
30
resources/server/api/ogc/templates/wfs3/leaflet_map.html
Normal file
30
resources/server/api/ogc/templates/wfs3/leaflet_map.html
Normal 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: '© <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>
|
6
resources/server/api/ogc/templates/wfs3/links.html
Normal file
6
resources/server/api/ogc/templates/wfs3/links.html
Normal 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>
|
@ -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"\//;
|
||||
|
@ -23,7 +23,7 @@ src/plugins/grass/qtermwidget/
|
||||
*.*.prepare
|
||||
*.sld
|
||||
.agignore
|
||||
|
||||
*.json
|
||||
|
||||
|
||||
#Specific files
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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" ) )
|
||||
|
@ -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
|
||||
|
@ -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() ) )
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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 )
|
||||
|
@ -84,7 +84,7 @@ class SERVER_EXPORT QgsServer
|
||||
|
||||
/**
|
||||
* Initialize Python
|
||||
* Note: not in Python bindings
|
||||
* \note not available in Python bindings
|
||||
*/
|
||||
void initPython();
|
||||
#endif
|
||||
|
37
src/server/qgsserverapi.cpp
Normal file
37
src/server/qgsserverapi.cpp
Normal 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
141
src/server/qgsserverapi.h
Normal 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
|
||||
|
||||
|
83
src/server/qgsserverapicontext.cpp
Normal file
83
src/server/qgsserverapicontext.cpp
Normal 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;
|
||||
}
|
113
src/server/qgsserverapicontext.h
Normal file
113
src/server/qgsserverapicontext.h
Normal 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
|
196
src/server/qgsserverapiutils.cpp
Normal file
196
src/server/qgsserverapiutils.cpp
Normal 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;
|
||||
}
|
||||
|
147
src/server/qgsserverapiutils.h
Normal file
147
src/server/qgsserverapiutils.h
Normal 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
|
@ -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
|
||||
|
152
src/server/qgsserverogcapi.cpp
Normal file
152
src/server/qgsserverogcapi.cpp
Normal 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;
|
||||
}
|
||||
|
||||
|
195
src/server/qgsserverogcapi.h
Normal file
195
src/server/qgsserverogcapi.h
Normal 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 link’s 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 link’s 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 link’s context is a part of a series, and that the previous in the series is the link targe
|
||||
next, //! Indicates that the link’s 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 link’s 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
|
494
src/server/qgsserverogcapihandler.cpp
Normal file
494
src/server/qgsserverogcapihandler.cpp
Normal 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() );
|
||||
}
|
337
src/server/qgsserverogcapihandler.h
Normal file
337
src/server/qgsserverogcapihandler.h
Normal 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
|
158
src/server/qgsserverquerystringparameter.cpp
Normal file
158
src/server/qgsserverquerystringparameter.cpp
Normal 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;
|
||||
}
|
155
src/server/qgsserverquerystringparameter.h
Normal file
155
src/server/qgsserverquerystringparameter.h
Normal 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
|
@ -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() );
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
62
src/server/services/wfs3/CMakeLists.txt
Normal file
62
src/server/services/wfs3/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
|
663
src/server/services/wfs3/openapi.json
Normal file
663
src/server/services/wfs3/openapi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
src/server/services/wfs3/qgswfs3.cpp
Normal file
65
src/server/services/wfs3/qgswfs3.cpp
Normal 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 ®istry, 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
|
||||
}
|
1200
src/server/services/wfs3/qgswfs3handlers.cpp
Normal file
1200
src/server/services/wfs3/qgswfs3handlers.cpp
Normal file
File diff suppressed because it is too large
Load Diff
239
src/server/services/wfs3/qgswfs3handlers.h
Normal file
239
src/server/services/wfs3/qgswfs3handlers.h
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
/***************************************************************************
|
||||
/**************************************************************************
|
||||
test_template.cpp
|
||||
--------------------------------------
|
||||
Date : Sun Sep 16 12:22:23 AKDT 2007
|
||||
|
@ -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)
|
||||
|
590
tests/src/python/test_qgsserver_api.py
Normal file
590
tests/src/python/test_qgsserver_api.py
Normal 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()
|
58
tests/src/python/test_qgsserver_apicontext.py
Normal file
58
tests/src/python/test_qgsserver_apicontext.py
Normal 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()
|
@ -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)
|
||||
|
@ -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)
|
||||
|
177
tests/src/server/testqgsserverquerystringparameter.cpp
Normal file
177
tests/src/server/testqgsserverquerystringparameter.cpp
Normal 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"
|
@ -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
|
||||
|
2
tests/testdata/qgis_server/api/test_api.json
vendored
Normal file
2
tests/testdata/qgis_server/api/test_api.json
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
"Test API"
|
3
tests/testdata/qgis_server/api/test_wfs3_api.json
vendored
Normal file
3
tests/testdata/qgis_server/api/test_wfs3_api.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"code":"Improperly configured error","description":"Project not found, please check your server configuration."}]
|
881
tests/testdata/qgis_server/api/test_wfs3_api_project.json
vendored
Normal file
881
tests/testdata/qgis_server/api/test_wfs3_api_project.json
vendored
Normal 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"
|
||||
}
|
76
tests/testdata/qgis_server/api/test_wfs3_collections_empty.html
vendored
Normal file
76
tests/testdata/qgis_server/api/test_wfs3_collections_empty.html
vendored
Normal 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>
|
||||
|
23
tests/testdata/qgis_server/api/test_wfs3_collections_empty.json
vendored
Normal file
23
tests/testdata/qgis_server/api/test_wfs3_collections_empty.json
vendored
Normal 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
Loading…
x
Reference in New Issue
Block a user