source: cows/trunk/cows/pylons/wfs_controller.py @ 6129

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/pylons/wfs_controller.py@6129
Revision 6129, 14.0 KB checked in by domlowe, 11 years ago (diff)

Modified GetPropertyValue? WFS method so that xpaths are resolved against CSML features with all xlink references fully resolved and replaced with inline content.

Line 
1# BSD Licence
2# Copyright (c) 2009, Science & Technology Facilities Council (STFC)
3# All rights reserved.
4#
5# See the LICENSE file in the source distribution of this software for
6# the full license text.
7
8"""
9WFS controller for OGC Web Services (OWS).
10
11@author: Dominic Lowe
12"""
13
14import re
15from cStringIO import StringIO
16from sets import Set
17from pylons import request, response, c
18import ConfigParser
19import xml.etree.ElementTree as etree
20import logging
21log = logging.getLogger(__name__)
22
23
24from cows.model.wfs import WfsFeatureSummary
25from cows.model.filterencoding import FEQueryProcessor
26from cows.model import PossibleValues, WGS84BoundingBox, BoundingBox, Contents
27from cows.pylons import ows_controller
28from cows.exceptions import *
29from cows import bbox_util
30from cows.service.imps.csmlbackend.config import config
31
32class WFSController(ows_controller.OWSController):
33    """
34    Subclass this controller in a pylons application and set the layerMapper
35    class attribute to implement a WFS. Each layer can be mapped to a Feature for the WFS.
36
37    @cvar layerMapper: an cows.service.wxs_iface.ILayerMapper object.
38   
39
40    """
41    layerMapper = None
42    _layerSlabCache = {}
43
44    #-------------------------------------------------------------------------
45    # Attributes required by OWSController
46
47    service = 'WFS'
48    owsOperations = (ows_controller.OWSController.owsOperations + ['DescribeFeatureType', 'GetFeature', 'DescribeStoredQueries','ListStoredQueries', 'GetPropertyValue'])
49    validVersions = ['1.1.0', '2.0.0']
50   
51
52    #-------------------------------------------------------------------------
53
54    def __before__(self, **kwargs):
55        """
56        This default implementation of __before__() will pass all routes
57        arguments to the layer mapper to retrieve a list of layers for
58        this WFS.
59
60        It will be called automatically by pylons before each action method.
61
62
63        """
64        log.debug('loading layers')
65        self.layers, self.featureset = self.layerMapper.map(**kwargs)
66        log.debug('Feature instances %s'%self.featureset.featureinstances)
67   
68        #-------------------------------------------------------------------------
69        # Methods implementing stubs in OWSController
70
71    def _renderCapabilities(self, version, format):
72        """
73        Renders capabilities document.
74        """
75        if version == '1.1.0':
76            t = ows_controller.templateLoader.load('wfs_capabilities_1_1_0.xml')
77        elif version == '2.0.0':
78            t = ows_controller.templateLoader.load('wfs_capabilities_2_0_0.xml')
79        else:
80            # We should never get here!  The framework should raise an exception before now.
81            raise RuntimeError("Version %s not supported" % version)
82       
83        return t.generate(c=c).render()
84
85    def _loadCapabilities(self):
86        """
87        @note: Assumes self.layers has already been created by __before__().
88        Builds capabilities document.
89
90        """
91        log.info('Loading WFS Capabilites')
92       
93        ows_controller.addOperation('GetFeature')
94        ows_controller.addOperation('DescribeFeature')
95        ows_controller.addOperation('DescribeStoredQueries')
96        ows_controller.addOperation('ListStoredQueries')
97        ows_controller.addOperation('GetPropertyValue')
98       
99       
100        featureInfoFormats = Set()
101
102        log.debug('Loading capabilities contents')
103        c.capabilities.contents = Contents()
104       
105       
106        ftlist={}
107        #       
108       
109        for layerName, layer in self.layers.items():
110            log.info('Loading layer %s' % layerName)
111#            log.info('feature type %s'%layer._feature)
112
113            wgs84BBox = WGS84BoundingBox(layer.wgs84BBox[:2],
114                                         layer.wgs84BBox[2:])
115           
116            ds = WfsFeatureSummary(keywords=layer.keywords, 
117                                   outputformats=layer.outputformats, 
118                                   identifier=layerName,
119                                   titles=[layer.title],
120                                   abstracts=[layer.abstract],                                   
121                                   wgs84BoundingBoxes=[wgs84BBox])
122
123            c.capabilities.contents.datasetSummaries.append(ds)
124       
125        #LayerMapper may optionally implement a datasetName attribute which
126        #will be tried if serviceIdentification/title is not supplied in capabilities config
127        if c.capabilities.serviceIdentification.titles[0] is None:
128            try:
129                c.capabilities.serviceIdentification.titles=[self.layerMapper.datasetName]
130            except AttributeError:
131                pass
132
133    def _getSchema(self, typename):
134        namespace = typename.split(':')[0]
135        schemalocation = conf
136       
137    def _parsetypename(self, typename):
138        """ parse feature type name into schema and name and return schema"""       
139        if typename not in self.layers.keys():
140            raise InvalidParameterValue('Invalid typename parameter: %s. Typename must consist of namespace and featuretype separated with a colon, as displayed in the GetCapabilities response.'%typename, 'typename')
141   
142        namespace, ft = typename.split(':')
143        wfsconfiglocation=config['wfsconfig']
144        wfscfg = ConfigParser.ConfigParser()
145        wfscfg.read(wfsconfiglocation)     
146        xmlschema=open(wfscfg.get('application_schemas', namespace)).read()     
147        log.debug('location of application schema %s' %(xmlschema))
148        return xmlschema
149   
150    def _runQuery(self, queryxml = None, storedqueryid=None, typename=None, maxfeatures=None,**kwargs):
151        """ this is used by both the GetFeature and GetPropertyValue methods to
152        run a wfs:query over a featurecollection and return a subset of the collection.
153        The query may be defined as an xml <wfs:query> or may be referenced by a StoredQuery_id
154        """
155        additionalobjects=[] #not implemented for filter encoding, just for storedqueries (e.g. subsetting)
156        if queryxml:
157            qp=FEQueryProcessor()
158            log.info('Sending query: %s to query processor '%queryxml)
159            resultset=qp.evaluate(self.featureset, queryxml)
160            log.info('Final resultset from query processor %s'%resultset)
161        elif storedqueryid:
162            storedquery, func=self.layerMapper.queryDescriptions.queries[storedqueryid]
163            storedqueryresult=func(self.featureset, **kwargs)
164            if len(storedqueryresult) == 2:
165                resultset=storedqueryresult[0]
166                additionalobjects=storedqueryresult[1]
167            else:
168                resultset=storedqueryresult
169            log.debug('Final resultset from stored query %s'%resultset)
170        else:           
171            #If neither query or storedquery_id are provided then return entire featureset, filtered on 'typename' and/or 'maxfeatures' if supplied.
172            resultset=self.featureset.getFeatureList()
173            if typename:
174                templist = resultset[:]
175                for feature in templist:   #remove any features of the wrong feature type
176                    if feature.featuretype != typename:
177                        resultset.remove(feature)
178                resultset=resultset
179                if maxfeatures: #reduce the response to the requested number of feature instances
180                    maxf=int(maxfeatures)
181                    if len(resultset) >maxf:
182                        resultset=resultset[:maxf]
183        return resultset, additionalobjects
184
185    def _applyXPath(self, xpath, features):
186        """ applies an xpath expression to a set of features and returns
187        a set of wfs:members containing property values expressed in the xpath"""
188        resultset=[]       
189        #if xpath is looking for an attribute, handle it differently - this should be unnecessary when migrated to element tree 1.3
190        #i.e. if of the form /somepath/path[@attribute]
191        attstart=xpath.find('[')
192        if attstart == -1: #no attribute           
193            for feature in features:
194                #need to deal with the underlying XML
195                #To make sure all xlink references are resolved correctly, convert feature to GML (which calls the CSML toXML() method then read back in with ElementTree
196                featureGML=feature.toGML()
197                featureXML=etree.fromstring(featureGML)               
198               
199                valuecomponent=featureXML.find(xpath)
200                if valuecomponent is not None:
201                    valstr=etree.tostring(valuecomponent)           
202                    resultset.append(valstr)
203        else:
204            #there is an attribute:
205            attributeName=xpath.split('[')[1][1:-1]
206            xpath=xpath.split('[')[0]
207            for feature in features:
208                #get the underlying XML
209                featureGML=feature.toGML()
210                featureXML=etree.fromstring(featureGML)
211                log.debug('xpath path %s'%xpath)
212                log.debug('attribute %s'%attributeName)
213                if len(xpath) ==0:
214                    valuecomponent=featureXML
215                else:
216                    valuecomponent=featureXML.find(xpath)
217                log.debug('valuecomponent %s'%valuecomponent)
218                if valuecomponent is not None:
219                    attribute=valuecomponent.get(attributeName)         
220                    resultset.append(attribute)           
221        log.debug(resultset)
222        return resultset     
223   
224   
225    def DescribeFeatureType(self):
226        """ DescribeFeatureType """
227        version = self.getOwsParam('version', default=self.validVersions[0])
228        if version not in self.validVersions:
229            raise InvalidParameterValue('Version %s not supported' % version,
230                                        'version')
231        typename=self.getOwsParam('typename')
232        ftschema =self._parsetypename(typename)
233        log.debug(self.layers.items())       
234               
235        outputformat=self.getOwsParam('outputformat', default='text/xml')
236       
237        #temporarily returns entire schema
238        #TODO: return single featuretype definition
239        msg  = ftschema
240        response.headers['content-type'] = 'text/xml'
241        return msg
242           
243 
244    def GetPropertyValue(self):
245        """ GetPropertyValue request - similar to get feature but only returns chosen property
246        values """
247        valueReference = self.getOwsParam('valueReference')
248        queryxml=self.getOwsParam('query',default=None)
249        storedqueryid=self.getOwsParam('storedquery_id', default=None)
250        typename=self.getOwsParam('typename', default=None)
251        #get any other parameters from self._owsParams and pass them to the stored query
252        otherparams={}
253        for key in self._owsParams.keys():
254            if key not in ['query', 'request', 'service', 'version', 'storedquery_id', 'typename', 'valuereference']:
255                otherparams[key]=self._owsParams[key]     
256        featureresultset, additionalobjects =self._runQuery(queryxml, storedqueryid, typename, **otherparams)       
257       
258        #Now need to take account of valueReferencexpath, and distill the
259        #resultset down to just the requested properties.
260        c.resultset=self._applyXPath(valueReference, featureresultset)
261       
262        response.headers['content-type'] = 'text/xml'
263        #TODO, new template for values
264        t = ows_controller.templateLoader.load('wfs_valuecollection.xml')
265        return t.generate(c=c).render() 
266 
267    def GetFeature(self):
268        """ GetFeature request
269        """
270        log.info('GET FEATURE Request made: %s'%request)
271        version = self.getOwsParam('version', default=self.validVersions[0])
272        if version not in self.validVersions:
273            raise InvalidParameterValue('Version %s not supported' % version,
274                                        'version')
275
276        #The GetFeature request may either use the 'query' filter encoding or a
277        #storedquery, but not both:     
278        #Parse the query to analyse the filters it contains
279        queryxml=self.getOwsParam('query',default=None)
280        storedqueryid=self.getOwsParam('storedquery_id', default=None)
281        typename=self.getOwsParam('typename', default=None)
282        maxfeatures=self.getOwsParam('maxfeatures', default=None)
283       
284        #retrieve any other parameters and pass them off to the stored query (they are ignored in the case of the queryxml option
285        otherparams={}
286        for key in self._owsParams.keys():
287            if key not in ['query', 'request', 'service', 'version', 'storedquery_id', 'typename', 'maxfeatures']:
288                otherparams[key]=self._owsParams[key]       
289        c.resultset, c.additionalobjects=self._runQuery(queryxml, storedqueryid, typename, maxfeatures, **otherparams)
290               
291        #Group resultset together in a wfs feature collection (use template)
292        response.headers['content-type'] = 'text/xml'
293        t = ows_controller.templateLoader.load('wfs_featurecollection.xml')
294        return t.generate(c=c).render()           
295
296   
297    def DescribeStoredQueries(self):
298        """ DescribeStoredQueries method. Takes zero or more stored query ids as args"""
299        allqueries=self.layerMapper.queryDescriptions.queries       
300        storedqueryid=self.getOwsParam('storedqueryid', default=None)
301        if storedqueryid is None:
302            c.storedqueries = self.layerMapper.queryDescriptions.queries
303        else:
304            c.storedqueries={}
305            for queryid in storedqueryid.split(','):
306                c.storedqueries[queryid]=self.layerMapper.queryDescriptions.queries[queryid]       
307        response.headers['Content-Type'] = 'text/xml'
308        t = ows_controller.templateLoader.load('wfs_describestoredqueries.xml')
309        return t.generate(c=c).render() 
310         
311    def ListStoredQueries(self):
312        """ DescribeStoredQueries method. Takes zero or more stored query ids as args"""
313        c.storedqueries = self.layerMapper.queryDescriptions.queries
314        t = ows_controller.templateLoader.load('wfs_liststoredqueries.xml')
315        response.headers['Content-Type'] = 'text/xml'
316        return t.generate(c=c).render() 
Note: See TracBrowser for help on using the repository browser.