Internal Architecture¶
When you follow the source of
the WFSView, WFSOperation
and BaseOwsRequest classes,
you’ll find that it’s written with extensibility in mind.
Extra operations can easily be added there.
You could even do that within your own projects and implementations.
A lot of the internal classes and object names are direct copies from the WFS 2.0 specification. By following these type definitions, a lot of the logic and code structure follows naturally.
Features and Fields¶
Each FeatureField is transformed into
an internal XsdElement object. The model field access happens
through get_value().
Note that the type can either reference either
an XsdTypes value or XsdComplexType object.
![digraph foo {
rankdir = LR;
FeatureField [shape=box]
XsdElement [shape=box]
type [shape=none, label=".type"]
model_attribute [shape=none, label=".model_attribute"]
get_value [shape=none, label=".get_value(instance)"]
FeatureField -> XsdElement [label=".xsd_element"]
XsdElement -> model_attribute
XsdElement -> type
XsdElement -> get_value
}](_images/graphviz-4f52ff26b80b4dcb0cbe42078f5aeb6f977d1f7e.png)
Each FeatureType is transformed into
an internal XsdComplexType definition:
![digraph foo {
rankdir = LR;
FeatureType [shape=box]
FeatureField [shape=box]
XsdComplexType [shape=box]
XsdElement [shape=box]
XsdAttribute [shape=box]
FeatureType -> FeatureField [label=".fields"]
FeatureType -> XsdComplexType [label=".xsd_type"]
XsdComplexType -> XsdElement [label=".elements"]
XsdComplexType -> XsdAttribute [label=".attributes"]
}](_images/graphviz-1977540cc767209b74e2f6be78405066edd1b6b1.png)
Every bit of the internal logic walks through the internal XSD structure. This includes:
Rendering GML/GeoJSON/CSV output.
Rendering the XML schema.
Resolving filter expressions.
Applying rendering projections.
Request Processing¶
To handle a request, several things happen:
Request parsing.
Query construction.
Query execution.
Output rendering.
To summarize:
![digraph foo {
rankdir = LR;
node [shape=box]
WFSView [label="WFSView"]
parsing [label="gisserver.parsers.wfs20"]
operations [label="gisserver.operations.wfs20"]
validate_request [label=".validate_request()", shape=none]
process_request [label=".process_request()", shape=none]
getdata [label="retrieve data...", shape=none]
WFSView -> parsing
WFSView -> operations
operations -> validate_request
operations -> process_request
process_request -> getdata
rendering [label="gisserver.output"]
process_request -> rendering
}](_images/graphviz-d9ff51d7876385a38177c7513b62641226d1575b.png)
Parsing the Request¶
The incoming XML POST message (e.g. a <wfs:GetFeature> request)
is translated as an internal “Abstract Syntax Tree” (AST)
which closely resembles all class names that the WFS and FES standards define.
This happens in gisserver.parsers.
The GET parameters are treated as Key-Value-Pairs (KVP). This is treated as a special case of the fully supported request notation that XML POST provides.
A GET request such as:
?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature
&TYPENAMES=app:restaurant
&FILTER=<Filter>...</Filter>
&PROPERTYNAME=app:id,app:name,app:location
&SORTBY=app:name ASC
or an XML-encoded request such as:
<wfs:GetFeature service="WFS" version="2.0.0" xmlns:wfs="..."
xmlns:gml="..." xmlns:fes="..." xmlns:app="...">
<wfs:Query typeNames="app:restaurant">
<wfs:PropertyName>app:id</wfs:PropertyName>
<wfs:PropertyName>app:name</wfs:PropertyName>
<wfs:PropertyName>app:location</wfs:PropertyName>
<fes:Filter>
<fes:And>
<fes:BBOX>
<gml:Envelope srsName="urn:ogc:def:crs:EPSG::28992">
<gml:lowerCorner>122400 486200</gml:lowerCorner>
<gml:upperCorner>122500 486300</gml:upperCorner>
</gml:Envelope>
</fes:BBOX>
<fes:PropertyIsGreaterThanOrEqualTo>
<fes:ValueReference>app:rating</fes:ValueReference>
<fes:Literal>3.0</fes:Literal>
</fes:PropertyIsGreaterThanOrEqualTo>
</fes:And>
</fes:Filter>
<fes:SortBy>
<fes:SortProperty>
<fes:ValueReference>app:name</fes:ValueReference>
<fes:SortOrder>ASC</fes:SortOrder>
</fes:SortProperty>
</fes:SortBy>
</wfs:Query>
<wfs:StoredQuery id="urn:ogc:def:query:OGC-WFS::GetFeatureById">
<wfs:Parameter name="ID">restaurant.123</wfs:Parameter>
</wfs:StoredQuery>
</wfs:GetFeature>
gives an AST somewhat like:
![digraph foo {
node [shape=box]
GetFeature [label="GetFeature\n<wfs:GetFeature>"]
GetFeature -> QueryExpression [label=".queries[...]"]
QueryExpression -> AdhocQuery [dir=back arrowtail=empty]
AdhocQuery [label="AdhocQuery\n<wfs:Query>"]
StoredQuery [label="StoredQuery\n<wfs:StoredQuery>"]
PropertyName [label="PropertyName\n<wfs:PropertyName>"]
Filter [label="Filter\n<fes:Filter>"]
SortBy [label="SortBy\n<fes:SortBy>"]
AdhocQuery -> PropertyName [label=".property_names"]
AdhocQuery -> Filter [label=".filter"]
AdhocQuery -> SortBy [label=".sortBy"]
BinaryLogicOperator [label="BinaryLogicOperator\n<fes:And>"]
BinarySpatialOperator [label="BinarySpatialOperator\n<fes:BBOX>"]
BinaryComparisonOperator [label="BinaryComparisonOperator\n<fes:PropertyIsEqualTo>"]
Envelope [label="Envelope\n<gml:Envelope>"]
ValueReference [label="ValueReference\n<fes:ValueReference>"]
Literal [label="Literal\n<fes:Literal>"]
Filter -> BinaryLogicOperator [label=".predicate"]
BinaryLogicOperator -> BinarySpatialOperator
BinaryLogicOperator -> BinaryComparisonOperator [label=".operands[...]"]
BinarySpatialOperator -> Envelope [label=".operand2"]
BinaryComparisonOperator -> ValueReference [label=".expression[0]"]
BinaryComparisonOperator -> Literal [label=".expression[1]"]
ValueReference2 [label="ValueReference\n<fes:ValueReference>"]
SortProperty [label="SortProperty\n<fes:SortProperty>"]
SortOrder [label="SortOrder\n<fes:SortOrder>"]
SortBy -> SortProperty
SortProperty -> ValueReference2
SortProperty -> SortOrder
QueryExpression -> StoredQuery [dir=back arrowtail=empty]
StoredQuery -> StoredQueryImplementation [label=".implementation"]
GetFeatureById
custom [label="..."]
StoredQueryImplementation -> GetFeatureById [dir=back arrowtail=empty]
StoredQueryImplementation -> custom [dir=back arrowtail=empty]
}](_images/graphviz-b68450622f487c2d1b8669d8a818eb3e485a7b50.png)
The top-level request parsing classes provide a from_xml() and from_kvp_request() classmethod.
This allows the initialization of these objects from the XML POST or KVP GET formats respectively.
The filter classes typically have a from_xml() only,
as the filter syntax is always written in XML.
All regular requests parameters such as ?FILTER=..., ?BBOX=..., ?SORTBY=...
and ?RESOURCEID=... are processed by the AdhocQuery class.
The StoredQuery node is used for ?STOREDQUERY_ID=... and <wfs:StoredQuery> requests.
Note
All the class names in this AST are mentioned in the WFS, FES and GML specifications. They are also found in the corresponding XSD schema.
The rare exception would be the AdhocQuery type, which is used for
<wfs:Query> element.
The spec extends it from fes:AbstractAdhocQueryExpression and fes:QueryExpression.
Dealing With Inheritance¶
Note most filter arguments support many different tags. The specification
defines the arguments as an Expression
or NonIdOperator subclass.
For example, <fes:PropertyIsEqualTo> accepts
both <fes:ValueReference>, <fes:Literal> or <fes:Function>.
The code solves this by calling Expression.child_from_xml().
It will resolve the correct child parsing class based on the tag name.
Query Construction¶
This parsed request is passed to the corresponding operation, which handles that request type.
For the gisserver.parsers.wfs20.GetFeature request,
there is a gisserver.operations.wfs20.GetFeature operation.
The GetFeature and GetPropertyValue operations will use the AST tree
to turn the query into a Django QuerySet.
This QuerySet becomes part of the FeatureCollection for rendering.
![digraph foo {
GetFeature [shape=box]
QueryExpression [shape=box]
FeatureCollection [shape=box]
SimpleFeatureCollection [shape=box]
validate_request [shape=none, label=".validate_request()", fontcolor="#1ba345"]
process_request [shape=none, label=".process_request()", fontcolor="#1ba345"]
get_results [shape=none, label=".get_results() / .get_hits()", fontcolor="#1ba345"]
get_type_names [shape=none, label="query.get_type_names()", fontcolor="#1ba345"]
get_queryset [shape=none, label=".get_queryset()", fontcolor="#1ba345"]
build_query [shape=none, label=".build_query(compiler)", fontcolor="#1ba345"]
compiler_get_queryset [shape=none, label="compiler.get_queryset()"]
GetFeature -> validate_request
GetFeature -> process_request
validate_request -> get_type_names
process_request -> get_results
get_results -> QueryExpression
QueryExpression -> get_queryset
get_queryset -> build_query
get_queryset -> compiler_get_queryset
get_results -> FeatureCollection [rank=same]
FeatureCollection -> SimpleFeatureCollection
}](_images/graphviz-453bdc4eef35a51baeada078bba210080f9f9727.png)
While walking through the AST, the CompiledQuery
collects all intermediate data needed to translate the query to a Django ORM call.
As that object is passed though all nodes of the filter,
each build...() function can add their lookups and annotations.
It produces the QuerySet objects:
Restaurant.objects \
.only('id', 'name', 'location')
.filter(
geometryfield__intersects=Polygon(...),
rating__gte=3.0
)
Restaurant.objects.filter(pk=123)
The operation wraps all these QuerySet objects in a SimpleFeatureCollection object.
All these collections become part of the final FeatureCollection.
These collections attempt to use queryset-iterator logic as much as possible,
unless it would cause multiple queries (such as needing the number_matched data early).
This information can now be passed to the output rendering.
Note
The names such as FeatureCollection
and SimpleFeatureCollection
all literally appear in the WFS 2.0 specification. They also correspond to the layout of the XML output.
Output Rendering¶
Each WFSOperation has a list
of OutputFormat objects:
class GetFeature(BaseWFSGetDataOperation):
def get_output_formats(self) -> list[OutputFormat]:
return [
OutputFormat("application/gml+xml", version="3.2", renderer_class=output.DBGML32Renderer),
OutputFormat("text/xml", subtype="gml/3.2.1", renderer_class=output.DBGML32Renderer),
OutputFormat("application/json", subtype="geojson", charset="utf-8", renderer_class=output.DBGeoJsonRenderer),
OutputFormat("text/csv", subtype="csv", charset="utf-8", renderer_class=output.DBCSVRenderer),
# OutputFormat("shapezip"),
# OutputFormat("application/zip"),
]
The OutputFormat class may reference
an renderer_class
which points to an OutputRenderer
(or CollectionOutputRenderer) subclass.
![digraph foo {
node [shape=box]
WFSOperation -> OutputFormat [label=".get_output_formats()"]
OutputFormat -> OutputRenderer [label=".renderer_class"]
OutputRenderer -> XmlOutputRenderer [dir=back arrowtail=empty]
OutputRenderer -> CollectionOutputRenderer [dir=back arrowtail=empty]
XmlOutputRenderer -> XmlSchemaRenderer [dir=back arrowtail=empty]
XmlOutputRenderer -> ListStoredQueriesRenderer [dir=back arrowtail=empty]
XmlOutputRenderer -> DescribeStoredQueriesRenderer [dir=back arrowtail=empty]
XmlOutputRenderer -> GML32Renderer [dir=back arrowtail=empty]
CollectionOutputRenderer -> GML32Renderer [dir=back arrowtail=empty]
CollectionOutputRenderer -> CSVRenderer [dir=back arrowtail=empty]
CollectionOutputRenderer -> GeoJsonRenderer [dir=back arrowtail=empty]
GML32Renderer -> DBGML32Renderer [dir=back arrowtail=empty]
CSVRenderer -> DBCSVRenderer [dir=back arrowtail=empty]
GeoJsonRenderer -> DBGeoJsonRenderer [dir=back arrowtail=empty]
}](_images/graphviz-efeaf7291483da87527175485a32717a9927af6c.png)
Various output formats have an DB-optimized version where the heavy rendering of the EWKT, JSON or GML fragments is done by the database server. Most output formats return a streaming response for performance.
Other WFS operations that also generate XML can implement a custom output renderer too.
The ListStoredQueriesRenderer is a nice example for rendering custom XML responses.
The output rendering also translates the fully qualified XML names
into shortened QName format (e.g. {http://www.opengis.net/gml/3.2}Point becomes <gml:Point>).
For fast development, the WFSOperation
may include the XmlTemplateMixin mixin
to render an XML template using Django templates. Currently, only GetCapabilities use that.
Applying the Projection¶
One special situation remains; the query also contains information about the “projection”.
That is, how the retrieved data should be transformed before rendering.
Most notably, the <wfs:PropertyName> determines that only certain members should be rendered.
Practically, this information is also used by the AdhocQuery
so it can retrieve less data. For the collection rendering,
our internal FeatureProjection provides all information
to render the data, including which elements or which coordinate transformation to apply.
It also detects that relations can be prefetched, to avoid N-query calls for related models.
Just before rendering, the QuerySet is passed to
a decorate_queryset function
of the output format.
Output Streaming¶
The output rendering will generate a Django HttpResponse with the contents.
When the renderer class returns a generator, it will return a StreamingHttpResponse
and hand that off to the WSGI server. The streaming rendering will help keep memory usage small.
Data is incrementally read from the database (in chunks),
and each chunk of rendered content (e.g. 40Kb) is written to the client.
Now the client can consume the data and present it!