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

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

Bug fix to prevent parameters being returned as unicode. New patch version 0.2.5p1.

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