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