source: cows/trunk/cows/pylons/ows_controller.py @ 5247

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/pylons/ows_controller.py@5247
Revision 5247, 9.4 KB checked in by spascoe, 11 years ago (diff)

Slightly better error reporting when not using Pylons debugger

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"""
9Base controller for OGC Web Services (OWS).
10
11@author: Stephen Pascoe
12@todo: Add pluggable security so replicate what was previously implemented
13    for the NDG discovery portal in ows_server.lib.BaseController.
14"""
15
16
17from pylons import request, response, config, c
18from pylons.controllers import WSGIController
19from pylons.templating import render
20from routes import url_for
21from paste.util.multidict import MultiDict
22
23from cows import exceptions as OWS_E
24from cows.util import negotiate_version, check_updatesequence
25from cows.builder import loadConfigFile
26from cows import helpers
27from cows.qs_util import parse_qsl
28from cows.model import *
29
30from genshi.template import TemplateLoader
31from pkg_resources import resource_filename
32
33try:
34    from xml.etree import ElementTree as ET
35except ImportError:
36    from elementtree import ElementTree as ET
37
38import logging
39log = logging.getLogger(__name__)
40
41# Instantiate Genshi template loader
42templateLoader = TemplateLoader(
43    resource_filename('cows.pylons', 'templates'),
44    auto_reload=True,
45    )
46
47# Configure
48EXCEPTION_TYPE = config.get('cows.exception_type', 'ogc')
49
50class OWSControllerBase(WSGIController):
51    """
52    Base class for all COWS Service controllers. 
53   
54    Subclasses should add supported operations to the cls.owsOperations attribute
55    and supply the a method of the same name (in UpperCammelCase). 
56   
57    When a request is received first self.__before__() will be called with the
58    parameters supplied by routes.  Override this method to configure the service
59    for a particular URL.  Then the operation method will be called
60    with the same parameters as self.__before__().  The operation parameters
61    (query_string) are not sent as method arguments.  Use self.owsParams or
62    self.getOwsParam() to retrieve these parameters.
63   
64    @ivar owsParams: A dictionary of parameters passed to the service.
65        Initially these comes from the query string but could come from
66        a HTTP POST in future.
67    @cvar owsOperations: A list of operation names
68   
69    """
70
71    owsOperations = []
72   
73    def __call__(self, environ, start_response):
74
75        self._loadOwsParams()
76
77        # If the EXCEPTION_TYPE is 'pylons' let Pylons catch any exceptions.
78        # Otherwise send an OGC exception report for any OWS_E.OwsError
79        if 'pylons' in EXCEPTION_TYPE:
80            self._fixOwsAction(environ)
81            return super(OWSControllerBase, self).__call__(environ, start_response)
82        else:
83            try:
84                self._fixOwsAction(environ)
85                return super(OWSControllerBase, self).__call__(environ, start_response)
86            except OWS_E.OwsError, e:
87                log.exception(e)
88
89                start_response('400 Bad Request', [('Content-type', 'text/xml')])
90                return [render_ows_exception(e)]
91            except Exception, e:
92                log.exception(e)
93
94                start_response('500 Internal Server Error', [('Content-type', 'text/plain')])
95                return ['Please see server logs for details']
96
97    def _loadOwsParams(self):
98        # All OWS parameter names are case insensitive.
99        self._owsParams = {}
100        log.debug('REQUEST: %s' % request)
101       
102        # request.params will use ';' as a QS separator which is not compatible
103        # with WPS-1.0.  Therefore we parse the QS ourselves.
104        qs = request.environ['QUERY_STRING']
105        params = MultiDict(parse_qsl(qs, semicolon_separator=False,
106                                     keep_blank_values=True, strict_parsing=False))
107       
108        for k in params:
109            if k.lower() == 'x':
110                self._owsParams['i'] = params[k]
111            elif k.lower() == 'y':
112                self._owsParams['j'] = params[k]
113            else:
114                self._owsParams[k.lower()] = params[k]
115                       
116    def _fixOwsAction(self, environ):
117        rdict = environ['pylons.routes_dict']
118       
119        # Override the Routes action from the request query parameter
120        action = self.getOwsParam('request')
121
122        # Check action is a method in self and is defined as an OWS operation
123        if action not in self.owsOperations:
124            raise OWS_E.InvalidParameterValue('request=%s not supported' % action,
125                                              'REQUEST')
126        rdict['action'] = action
127
128    def getOwsParam(self, param, **kwargs):
129        """
130        Returns the value of a OWS parameter passed to the operation.
131        If kwargs['default'] is given it is taken to be the default
132        value otherwise the parameter is treated as mandatory and an
133        exception is raised if the parameter is not present.
134
135        """
136        try:
137            return self._owsParams[param.lower()]
138        except KeyError:
139            if 'default' in kwargs:
140                return kwargs['default']
141            else:
142                raise OWS_E.MissingParameterValue('%s parameter is not specified' % param,
143                                                  param)
144
145#-----------------------------------------------------------------------------
146# Functions that populate c.capabilities
147
148def addOperation(opName, formats=[]):
149    ops = c.capabilities.operationsMetadata.operationDict
150    ops[opName] = helpers.operation(url_for(qualified=True, action="index")+'?', formats=formats)
151
152def addLayer(name, title, abstract, srss, bbox, dimensions={}):
153    """
154    @param dimensions: Dictionary of dictionaries D[k1][k2]=val where
155        k1 is dimension name, k2 is a keyword parameter to send to
156        helpers.wms_dimension and val is it's value.
157
158    @todo: The helpers interface is leaking through.  Could make cleaner.
159
160    """
161       
162    if c.capabilities.contents is None:
163        c.capabilities.contents = Contents()
164
165    layer = helpers.wms_layer(name, title, srss, bbox, abstract)
166
167    for k1, kwargs in dimensions.items():
168        dim = helpers.wms_dimension(**kwargs)
169        layer.dimensions[k1] = dim
170
171    c.capabilities.contents.datasetSummaries.append(layer)
172
173def initCapabilities():
174    """
175    Initialise the capabilities object c.capabilities.
176
177    By default the server-wide configuration file is loaded and
178    used to populate some standard metadata.  The GetCapabilites
179    operation is added.
180
181    """
182    # Load the basic ServiceMetadata from a config file
183    try:
184        configFile = config['cows.capabilities_config']
185    except KeyError:
186        configFile = config.get('ows_server.capabilities_config')
187    if configFile is None:
188        raise RuntimeError('No OWS configuration file')
189   
190    c.capabilities = loadConfigFile(configFile)
191
192    om = OperationsMetadata(operationDict={})
193    c.capabilities.operationsMetadata = om
194
195    addOperation('GetCapabilities', formats=['text/xml'])
196
197#-----------------------------------------------------------------------------
198
199class OWSController(OWSControllerBase):
200    """
201    Adds basic GetCapabilities response to OWSControllerBase.
202
203    @cvar service: If None does not enforce the SERVICE parameter.  Otherwise
204        raises exception if SERVICE is not correct on GetCapabilities request.
205    @cvar validVersions: A list of supported version numbers.  Automatic
206        version negotiation is performed according to this attribute.
207   
208    @ivar updateSequence: None if cache-control is not supported or an
209        updateSequence identifier.  This attribute should be set in the
210        controller's __before__() method.
211    """
212
213    owsOperations = ['GetCapabilities']
214
215    # Override these attributes to control how OWSController responds to
216    # GetCapabilities
217    service = None
218    validVersions = NotImplemented
219
220    # To enable cache control set this instance attribute in self.__before__().
221    updateSequence = None
222   
223    def GetCapabilities(self):
224
225        # Retrieve Operation parameters
226        service = self.getOwsParam('service')
227        version = self.getOwsParam('version', default=None)
228        format = self.getOwsParam('format', default='text/xml')
229        updateSequence = self.getOwsParam('updatesequence', default=None)
230
231        # Check update sequence
232        check_updatesequence(self.updateSequence, updateSequence)
233
234        # Do version negotiation
235        version = negotiate_version(self.validVersions, version)
236
237        # Get information required for the capabilities document
238        initCapabilities()
239        self._loadCapabilities()
240       
241        # Render the capabilities document       
242        response.headers['content-type'] = format
243        return self._renderCapabilities(version, format)
244
245
246    def _loadCapabilities(self):
247        """
248        Override in subclasses to populate c.capabilities with
249        operation and contents metadata.
250
251        """
252        pass
253
254    def _renderCapabilities(self, version, format):
255        """
256        Override in subclasses to render the capabilities document.
257        The response mime-type will already have been set.  Raise an
258        OWS exception if the format is not supported.
259
260        @param version: the version as a string
261        @param format: the format as a string
262       
263        @return: a template as expected by pylons.render()
264        """
265        raise NotImplementedError
266
267
268def render_ows_exception(e):
269    tmpl = templateLoader.load('exception_report.xml')
270    return str(tmpl.generate(report=e.report).render('xml'))
271
272
Note: See TracBrowser for help on using the repository browser.