~nchohan/appscale/zk3.3.4

« back to all changes in this revision

Viewing changes to AppServer/lib/protorpc/protorpc/webapp/service_handlers.py

  • Committer: Chris Bunch
  • Date: 2012-02-17 08:19:21 UTC
  • mfrom: (787.2.3 appscale-raj-merge)
  • Revision ID: cgb@cs.ucsb.edu-20120217081921-pakidyksaenlpzur
merged with main branch, gaining rabbitmq and upgrades for hbase, cassandra, and hypertable, as well as upgrading to gae 1.6.1 for python and go

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
# Copyright 2010 Google Inc.
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License");
 
6
# you may not use this file except in compliance with the License.
 
7
# You may obtain a copy of the License at
 
8
#
 
9
#     http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS,
 
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
14
# See the License for the specific language governing permissions and
 
15
# limitations under the License.
 
16
#
 
17
 
 
18
"""Handlers for remote services.
 
19
 
 
20
This module contains classes that may be used to build a service
 
21
on top of the App Engine Webapp framework.
 
22
 
 
23
The services request handler can be configured to handle requests in a number
 
24
of different request formats.  All different request formats must have a way
 
25
to map the request to the service handlers defined request message.Message
 
26
class.  The handler can also send a response in any format that can be mapped
 
27
from the response message.Message class.
 
28
 
 
29
Participants in an RPC:
 
30
 
 
31
  There are four classes involved with the life cycle of an RPC.
 
32
 
 
33
    Service factory: A user-defined service factory that is responsible for
 
34
      instantiating an RPC service.  The methods intended for use as RPC
 
35
      methods must be decorated by the 'remote' decorator.
 
36
 
 
37
    RPCMapper: Responsible for determining whether or not a specific request
 
38
      matches a particular RPC format and translating between the actual
 
39
      request/response and the underlying message types.  A single instance of
 
40
      an RPCMapper sub-class is required per service configuration.  Each
 
41
      mapper must be usable across multiple requests.
 
42
 
 
43
    ServiceHandler: A webapp.RequestHandler sub-class that responds to the
 
44
      webapp framework.  It mediates between the RPCMapper and service
 
45
      implementation class during a request.  As determined by the Webapp
 
46
      framework, a new ServiceHandler instance is created to handle each
 
47
      user request.  A handler is never used to handle more than one request.
 
48
 
 
49
    ServiceHandlerFactory: A class that is responsible for creating new,
 
50
      properly configured ServiceHandler instance for each request.  The
 
51
      factory is configured by providing it with a set of RPCMapper instances.
 
52
      When the Webapp framework invokes the service handler, the handler
 
53
      creates a new service class instance.  The service class instance is
 
54
      provided with a reference to the handler.  A single instance of an
 
55
      RPCMapper sub-class is required to configure each service.  Each mapper
 
56
      instance must be usable across multiple requests.
 
57
 
 
58
RPC mappers:
 
59
 
 
60
  RPC mappers translate between a single HTTP based RPC protocol and the
 
61
  underlying service implementation.  Each RPC mapper must configured
 
62
  with the following information to determine if it is an appropriate
 
63
  mapper for a given request:
 
64
 
 
65
    http_methods: Set of HTTP methods supported by handler.
 
66
 
 
67
    content_types: Set of supported content types.
 
68
 
 
69
    default_content_type: Default content type for handler responses.
 
70
 
 
71
  Built-in mapper implementations:
 
72
 
 
73
    URLEncodedRPCMapper: Matches requests that are compatible with post
 
74
      forms with the 'application/x-www-form-urlencoded' content-type
 
75
      (this content type is the default if none is specified.  It
 
76
      translates post parameters into request parameters.
 
77
 
 
78
    ProtobufRPCMapper: Matches requests that are compatible with post
 
79
      forms with the 'application/x-google-protobuf' content-type.  It
 
80
      reads the contents of a binary post request.
 
81
 
 
82
Public Exceptions:
 
83
  Error: Base class for service handler errors.
 
84
  ServiceConfigurationError: Raised when a service not correctly configured.
 
85
  RequestError: Raised by RPC mappers when there is an error in its request
 
86
    or request format.
 
87
  ResponseError: Raised by RPC mappers when there is an error in its response.
 
88
"""
 
89
 
 
90
__author__ = 'rafek@google.com (Rafe Kaplan)'
 
91
 
 
92
 
 
93
import array
 
94
import cgi
 
95
import itertools
 
96
import httplib
 
97
import logging
 
98
import re
 
99
import sys
 
100
import traceback
 
101
import urllib
 
102
import weakref
 
103
 
 
104
from google.appengine.ext import webapp
 
105
from google.appengine.ext.webapp import util as webapp_util
 
106
from .. import messages
 
107
from .. import protobuf
 
108
from .. import protojson
 
109
from .. import protourlencode
 
110
from .. import registry
 
111
from .. import remote
 
112
from .. import util
 
113
from . import forms
 
114
 
 
115
__all__ = [
 
116
    'Error',
 
117
    'RequestError',
 
118
    'ResponseError',
 
119
    'ServiceConfigurationError',
 
120
 
 
121
    'DEFAULT_REGISTRY_PATH',
 
122
 
 
123
    'ProtobufRPCMapper',
 
124
    'RPCMapper',
 
125
    'ServiceHandler',
 
126
    'ServiceHandlerFactory',
 
127
    'URLEncodedRPCMapper',
 
128
    'JSONRPCMapper',
 
129
    'service_mapping',
 
130
    'run_services',
 
131
]
 
132
 
 
133
 
 
134
class Error(Exception):
 
135
  """Base class for all errors in service handlers module."""
 
136
 
 
137
 
 
138
class ServiceConfigurationError(Error):
 
139
  """When service configuration is incorrect."""
 
140
 
 
141
 
 
142
class RequestError(Error):
 
143
  """Error occurred when building request."""
 
144
 
 
145
 
 
146
class ResponseError(Error):
 
147
  """Error occurred when building response."""
 
148
 
 
149
 
 
150
_URLENCODED_CONTENT_TYPE = protourlencode.CONTENT_TYPE
 
151
_PROTOBUF_CONTENT_TYPE = protobuf.CONTENT_TYPE
 
152
_JSON_CONTENT_TYPE = protojson.CONTENT_TYPE
 
153
 
 
154
_EXTRA_JSON_CONTENT_TYPES = ['application/x-javascript',
 
155
                             'text/javascript',
 
156
                             'text/x-javascript',
 
157
                             'text/x-json',
 
158
                             'text/json',
 
159
                            ]
 
160
 
 
161
# The whole method pattern is an optional regex.  It contains a single
 
162
# group used for mapping to the query parameter.  This is passed to the
 
163
# parameters of 'get' and 'post' on the ServiceHandler.
 
164
_METHOD_PATTERN = r'(?:\.([^?]*))?'
 
165
 
 
166
DEFAULT_REGISTRY_PATH = forms.DEFAULT_REGISTRY_PATH
 
167
 
 
168
 
 
169
class RPCMapper(object):
 
170
  """Interface to mediate between request and service object.
 
171
 
 
172
  Request mappers are implemented to support various types of
 
173
  RPC protocols.  It is responsible for identifying whether a
 
174
  given request matches a particular protocol, resolve the remote
 
175
  method to invoke and mediate between the request and appropriate
 
176
  protocol messages for the remote method.
 
177
  """
 
178
 
 
179
  @util.positional(4)
 
180
  def __init__(self,
 
181
               http_methods,
 
182
               default_content_type,
 
183
               protocol,
 
184
               content_types=None):
 
185
    """Constructor.
 
186
 
 
187
    Args:
 
188
      http_methods: Set of HTTP methods supported by mapper.
 
189
      default_content_type: Default content type supported by mapper.
 
190
      protocol: The protocol implementation.  Must implement encode_message and
 
191
        decode_message.
 
192
      content_types: Set of additionally supported content types.
 
193
    """
 
194
    self.__http_methods = frozenset(http_methods)
 
195
    self.__default_content_type = default_content_type
 
196
    self.__protocol = protocol
 
197
 
 
198
    if content_types is None:
 
199
      content_types = []
 
200
    self.__content_types = frozenset([self.__default_content_type] +
 
201
                                     content_types)
 
202
 
 
203
  @property
 
204
  def http_methods(self):
 
205
    return self.__http_methods
 
206
 
 
207
  @property
 
208
  def default_content_type(self):
 
209
    return self.__default_content_type
 
210
 
 
211
  @property
 
212
  def content_types(self):
 
213
    return self.__content_types
 
214
 
 
215
  def build_request(self, handler, request_type):
 
216
    """Build request message based on request.
 
217
 
 
218
    Each request mapper implementation is responsible for converting a
 
219
    request to an appropriate message instance.
 
220
 
 
221
    Args:
 
222
      handler: RequestHandler instance that is servicing request.
 
223
        Must be initialized with request object and been previously determined
 
224
        to matching the protocol of the RPCMapper.
 
225
      request_type: Message type to build.
 
226
 
 
227
    Returns:
 
228
      Instance of request_type populated by protocol buffer in request body.
 
229
 
 
230
    Raises:
 
231
      RequestError if the mapper implementation is not able to correctly
 
232
      convert the request to the appropriate message.
 
233
    """
 
234
    try:
 
235
      return self.__protocol.decode_message(request_type, handler.request.body)
 
236
    except (messages.ValidationError, messages.DecodeError), err:
 
237
      raise RequestError('Unable to parse request content: %s' % err)
 
238
 
 
239
  def build_response(self, handler, response, pad_string=False):
 
240
    """Build response based on service object response message.
 
241
 
 
242
    Each request mapper implementation is responsible for converting a
 
243
    response message to an appropriate handler response.
 
244
 
 
245
    Args:
 
246
      handler: RequestHandler instance that is servicing request.
 
247
        Must be initialized with request object and been previously determined
 
248
        to matching the protocol of the RPCMapper.
 
249
      response: Response message as returned from the service object.
 
250
 
 
251
    Raises:
 
252
      ResponseError if the mapper implementation is not able to correctly
 
253
      convert the message to an appropriate response.
 
254
    """
 
255
    try:
 
256
      encoded_message = self.__protocol.encode_message(response)
 
257
    except messages.ValidationError, err:
 
258
      raise ResponseError('Unable to encode message: %s' % err)
 
259
    else:
 
260
      handler.response.headers['Content-Type'] = self.default_content_type
 
261
      handler.response.out.write(encoded_message)
 
262
 
 
263
 
 
264
class ServiceHandlerFactory(object):
 
265
  """Factory class used for instantiating new service handlers.
 
266
 
 
267
  Normally a handler class is passed directly to the webapp framework
 
268
  so that it can be simply instantiated to handle a single request.
 
269
  The service handler, however, must be configured with additional
 
270
  information so that it knows how to instantiate a service object.
 
271
  This class acts the same as a normal RequestHandler class by
 
272
  overriding the __call__ method to correctly configures a ServiceHandler
 
273
  instance with a new service object.
 
274
 
 
275
  The factory must also provide a set of RPCMapper instances which
 
276
  examine a request to determine what protocol is being used and mediates
 
277
  between the request and the service object.
 
278
 
 
279
  The mapping of a service handler must have a single group indicating the
 
280
  part of the URL path that maps to the request method.  This group must
 
281
  exist but can be optional for the request (the group may be followed by
 
282
  '?' in the regular expression matching the request).
 
283
 
 
284
  Usage:
 
285
 
 
286
    stock_factory = ServiceHandlerFactory(StockService)
 
287
    ... configure stock_factory by adding RPCMapper instances ...
 
288
 
 
289
    application = webapp.WSGIApplication(
 
290
        [stock_factory.mapping('/stocks')])
 
291
 
 
292
  Default usage:
 
293
 
 
294
    application = webapp.WSGIApplication(
 
295
        [ServiceHandlerFactory.default(StockService).mapping('/stocks')])
 
296
  """
 
297
 
 
298
  def __init__(self, service_factory):
 
299
    """Constructor.
 
300
 
 
301
    Args:
 
302
      service_factory: Service factory to instantiate and provide to
 
303
        service handler.
 
304
    """
 
305
    self.__service_factory = service_factory
 
306
    self.__request_mappers = []
 
307
 
 
308
  def all_request_mappers(self):
 
309
    """Get all request mappers.
 
310
 
 
311
    Returns:
 
312
      Iterator of all request mappers used by this service factory.
 
313
    """
 
314
    return iter(self.__request_mappers)
 
315
 
 
316
  def add_request_mapper(self, mapper):
 
317
    """Add request mapper to end of request mapper list."""
 
318
    self.__request_mappers.append(mapper)
 
319
 
 
320
  def __call__(self):
 
321
    """Construct a new service handler instance."""
 
322
    return ServiceHandler(self, self.__service_factory())
 
323
 
 
324
  @property
 
325
  def service_factory(self):
 
326
    """Service factory associated with this factory."""
 
327
    return self.__service_factory
 
328
 
 
329
  @staticmethod
 
330
  def __check_path(path):
 
331
    """Check a path parameter.
 
332
 
 
333
    Make sure a provided path parameter is compatible with the
 
334
    webapp URL mapping.
 
335
 
 
336
    Args:
 
337
      path: Path to check.  This is a plain path, not a regular expression.
 
338
 
 
339
    Raises:
 
340
      ValueError if path does not start with /, path ends with /.
 
341
    """
 
342
    if path.endswith('/'):
 
343
      raise ValueError('Path %s must not end with /.' % path)
 
344
 
 
345
  def mapping(self, path):
 
346
    """Convenience method to map service to application.
 
347
 
 
348
    Args:
 
349
      path: Path to map service to.  It must be a simple path
 
350
        with a leading / and no trailing /.
 
351
 
 
352
    Returns:
 
353
      Mapping from service URL to service handler factory.
 
354
    """
 
355
    self.__check_path(path)
 
356
 
 
357
    service_url_pattern = r'(%s)%s' % (path, _METHOD_PATTERN)
 
358
 
 
359
    return service_url_pattern, self
 
360
 
 
361
  @classmethod
 
362
  def default(cls, service_factory, parameter_prefix=''):
 
363
    """Convenience method to map default factory configuration to application.
 
364
 
 
365
    Creates a standardized default service factory configuration that pre-maps
 
366
    the URL encoded protocol handler to the factory.
 
367
 
 
368
    Args:
 
369
      service_factory: Service factory to instantiate and provide to
 
370
        service handler.
 
371
      method_parameter: The name of the form parameter used to determine the
 
372
        method to invoke used by the URLEncodedRPCMapper.  If None, no
 
373
        parameter is used and the mapper will only match against the form
 
374
        path-name.  Defaults to 'method'.
 
375
      parameter_prefix: If provided, all the parameters in the form are
 
376
        expected to begin with that prefix by the URLEncodedRPCMapper.
 
377
 
 
378
    Returns:
 
379
      Mapping from service URL to service handler factory.
 
380
    """
 
381
    factory = cls(service_factory)
 
382
 
 
383
    factory.add_request_mapper(ProtobufRPCMapper())
 
384
    factory.add_request_mapper(JSONRPCMapper())
 
385
 
 
386
    return factory
 
387
 
 
388
 
 
389
class ServiceHandler(webapp.RequestHandler):
 
390
  """Web handler for RPC service.
 
391
 
 
392
  Overridden methods:
 
393
    get: All requests handled by 'handle' method.  HTTP method stored in
 
394
      attribute.  Takes remote_method parameter as derived from the URL mapping.
 
395
    post: All requests handled by 'handle' method.  HTTP method stored in
 
396
      attribute.  Takes remote_method parameter as derived from the URL mapping.
 
397
    redirect: Not implemented for this service handler.
 
398
 
 
399
  New methods:
 
400
    handle: Handle request for both GET and POST.
 
401
 
 
402
  Attributes (in addition to attributes in RequestHandler):
 
403
    service: Service instance associated with request being handled.
 
404
    method: Method of request.  Used by RPCMapper to determine match.
 
405
    remote_method: Sub-path as provided to the 'get' and 'post' methods.
 
406
  """
 
407
 
 
408
  def __init__(self, factory, service):
 
409
    """Constructor.
 
410
 
 
411
    Args:
 
412
      factory: Instance of ServiceFactory used for constructing new service
 
413
        instances used for handling requests.
 
414
      service: Service instance used for handling RPC.
 
415
    """
 
416
    self.__factory = factory
 
417
    self.__service = service
 
418
 
 
419
  @property
 
420
  def service(self):
 
421
    return self.__service
 
422
 
 
423
  def __show_info(self, service_path, remote_method):
 
424
    self.response.headers['content-type'] = 'text/plain; charset=utf-8'
 
425
    response_message = []
 
426
    if remote_method:
 
427
      response_message.append('%s.%s is a ProtoRPC method.\n\n' %(
 
428
        service_path, remote_method))
 
429
    else:
 
430
      response_message.append('%s is a ProtoRPC service.\n\n' % service_path)
 
431
    definition_name_function = getattr(self.__service, 'definition_name', None)
 
432
    if definition_name_function:
 
433
      definition_name = definition_name_function()
 
434
    else:
 
435
      definition_name = '%s.%s' % (self.__service.__module__,
 
436
                                   self.__service.__class__.__name__)
 
437
 
 
438
    response_message.append('Service %s\n\n' % definition_name)
 
439
    response_message.append('More about ProtoRPC: ')
 
440
      
 
441
    response_message.append('http://code.google.com/p/google-protorpc\n')
 
442
    self.response.out.write(util.pad_string(''.join(response_message)))
 
443
 
 
444
  def get(self, service_path, remote_method):
 
445
    """Handler method for GET requests.
 
446
 
 
447
    Args:
 
448
      service_path: Service path derived from request URL.
 
449
      remote_method: Sub-path after service path has been matched.
 
450
    """
 
451
    self.handle('GET', service_path, remote_method)
 
452
 
 
453
  def post(self, service_path, remote_method):
 
454
    """Handler method for POST requests.
 
455
 
 
456
    Args:
 
457
      service_path: Service path derived from request URL.
 
458
      remote_method: Sub-path after service path has been matched.
 
459
    """
 
460
    self.handle('POST', service_path, remote_method)
 
461
 
 
462
  def redirect(self, uri, permanent=False):
 
463
    """Not supported for services."""
 
464
    raise NotImplementedError('Services do not currently support redirection.')
 
465
 
 
466
  def __send_error(self,
 
467
                   http_code,
 
468
                   status_state,
 
469
                   error_message,
 
470
                   mapper,
 
471
                   error_name=None):
 
472
    status = remote.RpcStatus(state=status_state,
 
473
                              error_message=error_message,
 
474
                              error_name=error_name)
 
475
    mapper.build_response(self, status)
 
476
    self.response.headers['content-type'] = mapper.default_content_type
 
477
 
 
478
    logging.error(error_message)
 
479
    response_content = self.response.out.getvalue()
 
480
    padding = ' ' * max(0, 512 - len(response_content))
 
481
    self.response.out.write(padding)
 
482
 
 
483
    self.response.set_status(http_code, error_message)
 
484
 
 
485
  def __send_simple_error(self, code, message, pad=True):
 
486
    """Send error to caller without embedded message."""
 
487
    self.response.headers['content-type'] = 'text/plain; charset=utf-8'
 
488
    logging.error(message)
 
489
    self.response.set_status(code, message)
 
490
 
 
491
    response_message = httplib.responses.get(code, 'Unknown Error')
 
492
    if pad:
 
493
      response_message = util.pad_string(response_message)
 
494
    self.response.out.write(response_message)
 
495
 
 
496
  def __get_content_type(self):
 
497
    content_type = self.request.headers.get('content-type', None)
 
498
    if not content_type:
 
499
      content_type = self.request.environ.get('HTTP_CONTENT_TYPE', None)
 
500
    if not content_type:
 
501
      return None
 
502
 
 
503
    # Lop off parameters from the end (for example content-encoding)
 
504
    return content_type.split(';', 1)[0].lower()
 
505
 
 
506
  def __headers(self, content_type):
 
507
    for name in self.request.headers:
 
508
      name = name.lower()
 
509
      if name == 'content-type':
 
510
        value = content_type
 
511
      elif name == 'content-length':
 
512
        value = str(len(self.request.body))
 
513
      else:
 
514
        value = self.request.headers.get(name, '')
 
515
      yield name, value
 
516
 
 
517
  def handle(self, http_method, service_path, remote_method):
 
518
    """Handle a service request.
 
519
 
 
520
    The handle method will handle either a GET or POST response.
 
521
    It is up to the individual mappers from the handler factory to determine
 
522
    which request methods they can service.
 
523
 
 
524
    If the protocol is not recognized, the request does not provide a correct
 
525
    request for that protocol or the service object does not support the
 
526
    requested RPC method, will return error code 400 in the response.
 
527
 
 
528
    Args:
 
529
      http_method: HTTP method of request.
 
530
      service_path: Service path derived from request URL.
 
531
      remote_method: Sub-path after service path has been matched.
 
532
    """
 
533
    self.response.headers['x-content-type-options'] = 'nosniff'
 
534
    if not remote_method and http_method == 'GET':
 
535
      # Special case a normal get request, presumably via a browser.
 
536
      self.error(405)
 
537
      self.__show_info(service_path, remote_method)
 
538
      return
 
539
 
 
540
    content_type = self.__get_content_type()
 
541
 
 
542
    # Provide server state to the service.  If the service object does not have
 
543
    # an "initialize_request_state" method, will not attempt to assign state.
 
544
    try:
 
545
      state_initializer = self.service.initialize_request_state
 
546
    except AttributeError:
 
547
      pass
 
548
    else:
 
549
      server_port = self.request.environ.get('SERVER_PORT', None)
 
550
      if server_port:
 
551
        server_port = int(server_port)
 
552
 
 
553
      request_state = remote.HttpRequestState(
 
554
          remote_host=self.request.environ.get('REMOTE_HOST', None),
 
555
          remote_address=self.request.environ.get('REMOTE_ADDR', None),
 
556
          server_host=self.request.environ.get('SERVER_HOST', None),
 
557
          server_port=server_port,
 
558
          http_method=http_method,
 
559
          service_path=service_path,
 
560
          headers=list(self.__headers(content_type)))
 
561
      state_initializer(request_state)
 
562
 
 
563
    if not content_type:
 
564
      self.__send_simple_error(400, 'Invalid RPC request: missing content-type')
 
565
      return
 
566
 
 
567
    # Search for mapper to mediate request.
 
568
    for mapper in self.__factory.all_request_mappers():
 
569
      if content_type in mapper.content_types:
 
570
        break
 
571
    else:
 
572
      if http_method == 'GET':
 
573
        self.error(httplib.UNSUPPORTED_MEDIA_TYPE)
 
574
        self.__show_info(service_path, remote_method)
 
575
      else:
 
576
        self.__send_simple_error(httplib.UNSUPPORTED_MEDIA_TYPE,
 
577
                                 'Unsupported content-type: %s' % content_type)
 
578
      return
 
579
 
 
580
    try:
 
581
      if http_method not in mapper.http_methods:
 
582
        if http_method == 'GET':
 
583
          self.error(httplib.METHOD_NOT_ALLOWED)
 
584
          self.__show_info(service_path, remote_method)
 
585
        else:
 
586
          self.__send_simple_error(httplib.METHOD_NOT_ALLOWED,
 
587
                                   'Unsupported HTTP method: %s' % http_method)
 
588
        return
 
589
 
 
590
      try:
 
591
        try:
 
592
          method = getattr(self.service, remote_method)
 
593
          method_info = method.remote
 
594
        except AttributeError, err:
 
595
          self.__send_error(
 
596
          400, remote.RpcState.METHOD_NOT_FOUND_ERROR,
 
597
            'Unrecognized RPC method: %s' % remote_method,
 
598
            mapper)
 
599
          return
 
600
 
 
601
        request = mapper.build_request(self, method_info.request_type)
 
602
      except (RequestError, messages.DecodeError), err:
 
603
        self.__send_error(400,
 
604
                          remote.RpcState.REQUEST_ERROR,
 
605
                          'Error parsing ProtoRPC request (%s)' % err,
 
606
                          mapper)
 
607
        return
 
608
 
 
609
      try:
 
610
        response = method(request)
 
611
      except remote.ApplicationError, err:
 
612
        self.__send_error(400,
 
613
                          remote.RpcState.APPLICATION_ERROR,
 
614
                          err.message,
 
615
                          mapper,
 
616
                          err.error_name)
 
617
        return
 
618
 
 
619
      mapper.build_response(self, response)
 
620
    except Exception, err:
 
621
      logging.error('An unexpected error occured when handling RPC: %s',
 
622
                    err, exc_info=1)
 
623
 
 
624
      self.__send_error(500,
 
625
                        remote.RpcState.SERVER_ERROR,
 
626
                        'Internal Server Error',
 
627
                        mapper)
 
628
      return
 
629
 
 
630
 
 
631
# TODO(rafek): Support tag-id only forms.
 
632
class URLEncodedRPCMapper(RPCMapper):
 
633
  """Request mapper for application/x-www-form-urlencoded forms.
 
634
 
 
635
  This mapper is useful for building forms that can invoke RPC.  Many services
 
636
  are also configured to work using URL encoded request information because
 
637
  of its perceived ease of programming and debugging.
 
638
 
 
639
  The mapper must be provided with at least method_parameter or
 
640
  remote_method_pattern so that it is possible to determine how to determine the
 
641
  requests RPC method.  If both are provided, the service will respond to both
 
642
  method request types, however, only one may be present in a given request.
 
643
  If both types are detected, the request will not match.
 
644
  """
 
645
 
 
646
  def __init__(self, parameter_prefix=''):
 
647
    """Constructor.
 
648
 
 
649
    Args:
 
650
      parameter_prefix: If provided, all the parameters in the form are
 
651
        expected to begin with that prefix.
 
652
    """
 
653
    # Private attributes:
 
654
    #   __parameter_prefix: parameter prefix as provided by constructor
 
655
    #     parameter.
 
656
    super(URLEncodedRPCMapper, self).__init__(['POST'],
 
657
                                              _URLENCODED_CONTENT_TYPE,
 
658
                                              self)
 
659
    self.__parameter_prefix = parameter_prefix
 
660
 
 
661
  def encode_message(self, message):
 
662
    """Encode a message using parameter prefix.
 
663
 
 
664
    Args:
 
665
      message: Message to URL Encode.
 
666
 
 
667
    Returns:
 
668
      URL encoded message.
 
669
    """
 
670
    return protourlencode.encode_message(message,
 
671
                                         prefix=self.__parameter_prefix)
 
672
 
 
673
  @property
 
674
  def parameter_prefix(self):
 
675
    """Prefix all form parameters are expected to begin with."""
 
676
    return self.__parameter_prefix
 
677
 
 
678
  def build_request(self, handler, request_type):
 
679
    """Build request from URL encoded HTTP request.
 
680
 
 
681
    Constructs message from names of URL encoded parameters.  If this service
 
682
    handler has a parameter prefix, parameters must begin with it or are
 
683
    ignored.
 
684
 
 
685
    Args:
 
686
      handler: RequestHandler instance that is servicing request.
 
687
      request_type: Message type to build.
 
688
 
 
689
    Returns:
 
690
      Instance of request_type populated by protocol buffer in request
 
691
        parameters.
 
692
 
 
693
    Raises:
 
694
      RequestError if message type contains nested message field or repeated
 
695
      message field.  Will raise RequestError if there are any repeated
 
696
      parameters.
 
697
    """
 
698
    request = request_type()
 
699
    builder = protourlencode.URLEncodedRequestBuilder(
 
700
        request, prefix=self.__parameter_prefix)
 
701
    for argument in sorted(handler.request.arguments()):
 
702
      values = handler.request.get_all(argument)
 
703
      try:
 
704
        builder.add_parameter(argument, values)
 
705
      except messages.DecodeError, err:
 
706
        raise RequestError(str(err))
 
707
    return request
 
708
 
 
709
 
 
710
class ProtobufRPCMapper(RPCMapper):
 
711
  """Request mapper for application/x-protobuf service requests.
 
712
 
 
713
  This mapper will parse protocol buffer from a POST body and return the request
 
714
  as a protocol buffer.
 
715
  """
 
716
 
 
717
  def __init__(self):
 
718
    super(ProtobufRPCMapper, self).__init__(['POST'],
 
719
                                            _PROTOBUF_CONTENT_TYPE,
 
720
                                            protobuf)
 
721
 
 
722
 
 
723
class JSONRPCMapper(RPCMapper):
 
724
  """Request mapper for application/x-protobuf service requests.
 
725
 
 
726
  This mapper will parse protocol buffer from a POST body and return the request
 
727
  as a protocol buffer.
 
728
  """
 
729
 
 
730
  def __init__(self):
 
731
    super(JSONRPCMapper, self).__init__(
 
732
        ['POST'],
 
733
        _JSON_CONTENT_TYPE,
 
734
        protojson,
 
735
        content_types=_EXTRA_JSON_CONTENT_TYPES)
 
736
 
 
737
 
 
738
def service_mapping(services,
 
739
                    registry_path=DEFAULT_REGISTRY_PATH):
 
740
  """Create a services mapping for use with webapp.
 
741
 
 
742
  Creates basic default configuration and registration for ProtoRPC services.
 
743
  Each service listed in the service mapping has a standard service handler
 
744
  factory created for it.
 
745
 
 
746
  The list of mappings can either be an explicit path to service mapping or
 
747
  just services.  If mappings are just services, they will automatically
 
748
  be mapped to their default name.  For exampel:
 
749
 
 
750
    package = 'my_package'
 
751
 
 
752
    class MyService(remote.Service):
 
753
      ...
 
754
 
 
755
    server_mapping([('/my_path', MyService),  # Maps to /my_path
 
756
                    MyService,                # Maps to /my_package/MyService
 
757
                   ])
 
758
 
 
759
  Specifying a service mapping:
 
760
 
 
761
    Normally services are mapped to URL paths by specifying a tuple
 
762
    (path, service):
 
763
      path: The path the service resides on.
 
764
      service: The service class or service factory for creating new instances
 
765
        of the service.  For more information about service factories, please
 
766
        see remote.Service.new_factory.
 
767
 
 
768
    If no tuple is provided, and therefore no path specified, a default path
 
769
    is calculated by using the fully qualified service name using a URL path
 
770
    separator for each of its components instead of a '.'.
 
771
 
 
772
  Args:
 
773
    services: Can be service type, service factory or string definition name of
 
774
        service being mapped or list of tuples (path, service):
 
775
      path: Path on server to map service to.
 
776
      service: Service type, service factory or string definition name of
 
777
        service being mapped.
 
778
      Can also be a dict.  If so, the keys are treated as the path and values as
 
779
      the service.
 
780
    registry_path: Path to give to registry service.  Use None to disable
 
781
      registry service.
 
782
 
 
783
  Returns:
 
784
    List of tuples defining a mapping of request handlers compatible with a
 
785
    webapp application.
 
786
 
 
787
  Raises:
 
788
    ServiceConfigurationError when duplicate paths are provided.
 
789
  """
 
790
  if isinstance(services, dict):
 
791
    services = services.iteritems()
 
792
  mapping = []
 
793
  registry_map = {}
 
794
 
 
795
  if registry_path is not None:
 
796
    registry_service = registry.RegistryService.new_factory(registry_map)
 
797
    services = list(services) + [(registry_path, registry_service)]
 
798
    mapping.append((registry_path + r'/form(?:/)?',
 
799
                    forms.FormsHandler.new_factory(registry_path)))
 
800
    mapping.append((registry_path + r'/form/(.+)', forms.ResourceHandler))
 
801
 
 
802
  paths = set()
 
803
  for service_item in services:
 
804
    infer_path = not isinstance(service_item, (list, tuple))
 
805
    if infer_path:
 
806
      service = service_item
 
807
    else:
 
808
      service = service_item[1]
 
809
 
 
810
    service_class = getattr(service, 'service_class', service)
 
811
 
 
812
    if infer_path:
 
813
      path = '/' + service_class.definition_name().replace('.', '/')
 
814
    else:
 
815
      path = service_item[0]
 
816
 
 
817
    if path in paths:
 
818
      raise ServiceConfigurationError(
 
819
        'Path %r is already defined in service mapping' % path.encode('utf-8'))
 
820
    else:
 
821
      paths.add(path)
 
822
 
 
823
    # Create service mapping for webapp.
 
824
    new_mapping = ServiceHandlerFactory.default(service).mapping(path)
 
825
    mapping.append(new_mapping)
 
826
 
 
827
    # Update registry with service class.
 
828
    registry_map[path] = service_class
 
829
 
 
830
  return mapping
 
831
 
 
832
 
 
833
def run_services(services,
 
834
                 registry_path=DEFAULT_REGISTRY_PATH):
 
835
  """Handle CGI request using service mapping.
 
836
 
 
837
  Args:
 
838
    Same as service_mapping.
 
839
  """
 
840
  mappings = service_mapping(services, registry_path=registry_path)
 
841
  application = webapp.WSGIApplication(mappings)
 
842
  webapp_util.run_wsgi_app(application)