#!/usr/bin/env python
#
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Handlers for remote services.

This module contains classes that may be used to build a service
on top of the App Engine Webapp framework.

The services request handler can be configured to handle requests in a number
of different request formats.  All different request formats must have a way
to map the request to the service handlers defined request message.Message
class.  The handler can also send a response in any format that can be mapped
from the response message.Message class.

Participants in an RPC:

  There are four classes involved with the life cycle of an RPC.

    Service factory: A user-defined service factory that is responsible for
      instantiating an RPC service.  The methods intended for use as RPC
      methods must be decorated by the 'remote' decorator.

    RPCMapper: Responsible for determining whether or not a specific request
      matches a particular RPC format and translating between the actual
      request/response and the underlying message types.  A single instance of
      an RPCMapper sub-class is required per service configuration.  Each
      mapper must be usable across multiple requests.

    ServiceHandler: A webapp.RequestHandler sub-class that responds to the
      webapp framework.  It mediates between the RPCMapper and service
      implementation class during a request.  As determined by the Webapp
      framework, a new ServiceHandler instance is created to handle each
      user request.  A handler is never used to handle more than one request.

    ServiceHandlerFactory: A class that is responsible for creating new,
      properly configured ServiceHandler instance for each request.  The
      factory is configured by providing it with a set of RPCMapper instances.
      When the Webapp framework invokes the service handler, the handler
      creates a new service class instance.  The service class instance is
      provided with a reference to the handler.  A single instance of an
      RPCMapper sub-class is required to configure each service.  Each mapper
      instance must be usable across multiple requests.

RPC mappers:

  RPC mappers translate between a single HTTP based RPC protocol and the
  underlying service implementation.  Each RPC mapper must configured
  with the following information to determine if it is an appropriate
  mapper for a given request:

    http_methods: Set of HTTP methods supported by handler.

    content_types: Set of supported content types.

    default_content_type: Default content type for handler responses.

  Built-in mapper implementations:

    URLEncodedRPCMapper: Matches requests that are compatible with post
      forms with the 'application/x-www-form-urlencoded' content-type
      (this content type is the default if none is specified.  It
      translates post parameters into request parameters.

    ProtobufRPCMapper: Matches requests that are compatible with post
      forms with the 'application/x-google-protobuf' content-type.  It
      reads the contents of a binary post request.

Public Exceptions:
  Error: Base class for service handler errors.
  ServiceConfigurationError: Raised when a service not correctly configured.
  RequestError: Raised by RPC mappers when there is an error in its request
    or request format.
  ResponseError: Raised by RPC mappers when there is an error in its response.
"""
import six

__author__ = 'rafek@google.com (Rafe Kaplan)'

import six.moves.http_client
import logging

from .google_imports import webapp
from .google_imports import webapp_util
from .. import messages
from .. import protobuf
from .. import protojson
from .. import protourlencode
from .. import registry
from .. import remote
from .. import util
from . import forms

__all__ = [
    'Error',
    'RequestError',
    'ResponseError',
    'ServiceConfigurationError',

    'DEFAULT_REGISTRY_PATH',

    'ProtobufRPCMapper',
    'RPCMapper',
    'ServiceHandler',
    'ServiceHandlerFactory',
    'URLEncodedRPCMapper',
    'JSONRPCMapper',
    'service_mapping',
    'run_services',
]


class Error(Exception):
  """Base class for all errors in service handlers module."""


class ServiceConfigurationError(Error):
  """When service configuration is incorrect."""


class RequestError(Error):
  """Error occurred when building request."""


class ResponseError(Error):
  """Error occurred when building response."""


_URLENCODED_CONTENT_TYPE = protourlencode.CONTENT_TYPE
_PROTOBUF_CONTENT_TYPE = protobuf.CONTENT_TYPE
_JSON_CONTENT_TYPE = protojson.CONTENT_TYPE

_EXTRA_JSON_CONTENT_TYPES = ['application/x-javascript',
                             'text/javascript',
                             'text/x-javascript',
                             'text/x-json',
                             'text/json',
                            ]

# The whole method pattern is an optional regex.  It contains a single
# group used for mapping to the query parameter.  This is passed to the
# parameters of 'get' and 'post' on the ServiceHandler.
_METHOD_PATTERN = r'(?:\.([^?]*))?'

DEFAULT_REGISTRY_PATH = forms.DEFAULT_REGISTRY_PATH


class RPCMapper(object):
  """Interface to mediate between request and service object.

  Request mappers are implemented to support various types of
  RPC protocols.  It is responsible for identifying whether a
  given request matches a particular protocol, resolve the remote
  method to invoke and mediate between the request and appropriate
  protocol messages for the remote method.
  """

  @util.positional(4)
  def __init__(self,
               http_methods,
               default_content_type,
               protocol,
               content_types=None):
    """Constructor.

    Args:
      http_methods: Set of HTTP methods supported by mapper.
      default_content_type: Default content type supported by mapper.
      protocol: The protocol implementation.  Must implement encode_message and
        decode_message.
      content_types: Set of additionally supported content types.
    """
    self.__http_methods = frozenset(http_methods)
    self.__default_content_type = default_content_type
    self.__protocol = protocol

    if content_types is None:
      content_types = []
    self.__content_types = frozenset([self.__default_content_type] +
                                     content_types)

  @property
  def http_methods(self):
    return self.__http_methods

  @property
  def default_content_type(self):
    return self.__default_content_type

  @property
  def content_types(self):
    return self.__content_types

  def build_request(self, handler, request_type):
    """Build request message based on request.

    Each request mapper implementation is responsible for converting a
    request to an appropriate message instance.

    Args:
      handler: RequestHandler instance that is servicing request.
        Must be initialized with request object and been previously determined
        to matching the protocol of the RPCMapper.
      request_type: Message type to build.

    Returns:
      Instance of request_type populated by protocol buffer in request body.

    Raises:
      RequestError if the mapper implementation is not able to correctly
      convert the request to the appropriate message.
    """
    try:
      return self.__protocol.decode_message(request_type, handler.request.body)
    except (messages.ValidationError, messages.DecodeError) as err:
      raise RequestError('Unable to parse request content: %s' % err)

  def build_response(self, handler, response, pad_string=False):
    """Build response based on service object response message.

    Each request mapper implementation is responsible for converting a
    response message to an appropriate handler response.

    Args:
      handler: RequestHandler instance that is servicing request.
        Must be initialized with request object and been previously determined
        to matching the protocol of the RPCMapper.
      response: Response message as returned from the service object.

    Raises:
      ResponseError if the mapper implementation is not able to correctly
      convert the message to an appropriate response.
    """
    try:
      encoded_message = self.__protocol.encode_message(response)
    except messages.ValidationError as err:
      raise ResponseError('Unable to encode message: %s' % err)
    else:
      handler.response.headers['Content-Type'] = self.default_content_type
      handler.response.out.write(encoded_message)


class ServiceHandlerFactory(object):
  """Factory class used for instantiating new service handlers.

  Normally a handler class is passed directly to the webapp framework
  so that it can be simply instantiated to handle a single request.
  The service handler, however, must be configured with additional
  information so that it knows how to instantiate a service object.
  This class acts the same as a normal RequestHandler class by
  overriding the __call__ method to correctly configures a ServiceHandler
  instance with a new service object.

  The factory must also provide a set of RPCMapper instances which
  examine a request to determine what protocol is being used and mediates
  between the request and the service object.

  The mapping of a service handler must have a single group indicating the
  part of the URL path that maps to the request method.  This group must
  exist but can be optional for the request (the group may be followed by
  '?' in the regular expression matching the request).

  Usage:

    stock_factory = ServiceHandlerFactory(StockService)
    ... configure stock_factory by adding RPCMapper instances ...

    application = webapp.WSGIApplication(
        [stock_factory.mapping('/stocks')])

  Default usage:

    application = webapp.WSGIApplication(
        [ServiceHandlerFactory.default(StockService).mapping('/stocks')])
  """

  def __init__(self, service_factory):
    """Constructor.

    Args:
      service_factory: Service factory to instantiate and provide to
        service handler.
    """
    self.__service_factory = service_factory
    self.__request_mappers = []

  def all_request_mappers(self):
    """Get all request mappers.

    Returns:
      Iterator of all request mappers used by this service factory.
    """
    return iter(self.__request_mappers)

  def add_request_mapper(self, mapper):
    """Add request mapper to end of request mapper list."""
    self.__request_mappers.append(mapper)

  def __call__(self):
    """Construct a new service handler instance."""
    return ServiceHandler(self, self.__service_factory())

  @property
  def service_factory(self):
    """Service factory associated with this factory."""
    return self.__service_factory

  @staticmethod
  def __check_path(path):
    """Check a path parameter.

    Make sure a provided path parameter is compatible with the
    webapp URL mapping.

    Args:
      path: Path to check.  This is a plain path, not a regular expression.

    Raises:
      ValueError if path does not start with /, path ends with /.
    """
    if path.endswith('/'):
      raise ValueError('Path %s must not end with /.' % path)

  def mapping(self, path):
    """Convenience method to map service to application.

    Args:
      path: Path to map service to.  It must be a simple path
        with a leading / and no trailing /.

    Returns:
      Mapping from service URL to service handler factory.
    """
    self.__check_path(path)

    service_url_pattern = r'(%s)%s' % (path, _METHOD_PATTERN)

    return service_url_pattern, self

  @classmethod
  def default(cls, service_factory, parameter_prefix=''):
    """Convenience method to map default factory configuration to application.

    Creates a standardized default service factory configuration that pre-maps
    the URL encoded protocol handler to the factory.

    Args:
      service_factory: Service factory to instantiate and provide to
        service handler.
      method_parameter: The name of the form parameter used to determine the
        method to invoke used by the URLEncodedRPCMapper.  If None, no
        parameter is used and the mapper will only match against the form
        path-name.  Defaults to 'method'.
      parameter_prefix: If provided, all the parameters in the form are
        expected to begin with that prefix by the URLEncodedRPCMapper.

    Returns:
      Mapping from service URL to service handler factory.
    """
    factory = cls(service_factory)

    factory.add_request_mapper(ProtobufRPCMapper())
    factory.add_request_mapper(JSONRPCMapper())

    return factory


class ServiceHandler(webapp.RequestHandler):
  """Web handler for RPC service.

  Overridden methods:
    get: All requests handled by 'handle' method.  HTTP method stored in
      attribute.  Takes remote_method parameter as derived from the URL mapping.
    post: All requests handled by 'handle' method.  HTTP method stored in
      attribute.  Takes remote_method parameter as derived from the URL mapping.
    redirect: Not implemented for this service handler.

  New methods:
    handle: Handle request for both GET and POST.

  Attributes (in addition to attributes in RequestHandler):
    service: Service instance associated with request being handled.
    method: Method of request.  Used by RPCMapper to determine match.
    remote_method: Sub-path as provided to the 'get' and 'post' methods.
  """

  def __init__(self, factory, service):
    """Constructor.

    Args:
      factory: Instance of ServiceFactory used for constructing new service
        instances used for handling requests.
      service: Service instance used for handling RPC.
    """
    self.__factory = factory
    self.__service = service

  @property
  def service(self):
    return self.__service

  def __show_info(self, service_path, remote_method):
    self.response.headers['content-type'] = 'text/plain; charset=utf-8'
    response_message = []
    if remote_method:
      response_message.append('%s.%s is a ProtoRPC method.\n\n' %(
        service_path, remote_method))
    else:
      response_message.append('%s is a ProtoRPC service.\n\n' % service_path)
    definition_name_function = getattr(self.__service, 'definition_name', None)
    if definition_name_function:
      definition_name = definition_name_function()
    else:
      definition_name = '%s.%s' % (self.__service.__module__,
                                   self.__service.__class__.__name__)

    response_message.append('Service %s\n\n' % definition_name)
    response_message.append('More about ProtoRPC: ')

    response_message.append('http://code.google.com/p/google-protorpc\n')
    self.response.out.write(util.pad_string(''.join(response_message)))

  def get(self, service_path, remote_method):
    """Handler method for GET requests.

    Args:
      service_path: Service path derived from request URL.
      remote_method: Sub-path after service path has been matched.
    """
    self.handle('GET', service_path, remote_method)

  def post(self, service_path, remote_method):
    """Handler method for POST requests.

    Args:
      service_path: Service path derived from request URL.
      remote_method: Sub-path after service path has been matched.
    """
    self.handle('POST', service_path, remote_method)

  def redirect(self, uri, permanent=False):
    """Not supported for services."""
    raise NotImplementedError('Services do not currently support redirection.')

  def __send_error(self,
                   http_code,
                   status_state,
                   error_message,
                   mapper,
                   error_name=None):
    status = remote.RpcStatus(state=status_state,
                              error_message=error_message,
                              error_name=error_name)
    mapper.build_response(self, status)
    self.response.headers['content-type'] = mapper.default_content_type

    logging.error(error_message)
    response_content = self.response.out.getvalue()
    padding = ' ' * max(0, 512 - len(response_content))
    self.response.out.write(padding)

    self.response.set_status(http_code, error_message)

  def __send_simple_error(self, code, message, pad=True):
    """Send error to caller without embedded message."""
    self.response.headers['content-type'] = 'text/plain; charset=utf-8'
    logging.error(message)
    self.response.set_status(code, message)

    response_message = six.moves.http_client.responses.get(code, 'Unknown Error')
    if pad:
      response_message = util.pad_string(response_message)
    self.response.out.write(response_message)

  def __get_content_type(self):
    content_type = self.request.headers.get('content-type', None)
    if not content_type:
      content_type = self.request.environ.get('HTTP_CONTENT_TYPE', None)
    if not content_type:
      return None

    # Lop off parameters from the end (for example content-encoding)
    return content_type.split(';', 1)[0].lower()

  def __headers(self, content_type):
    for name in self.request.headers:
      name = name.lower()
      if name == 'content-type':
        value = content_type
      elif name == 'content-length':
        value = str(len(self.request.body))
      else:
        value = self.request.headers.get(name, '')
      yield name, value

  def handle(self, http_method, service_path, remote_method):
    """Handle a service request.

    The handle method will handle either a GET or POST response.
    It is up to the individual mappers from the handler factory to determine
    which request methods they can service.

    If the protocol is not recognized, the request does not provide a correct
    request for that protocol or the service object does not support the
    requested RPC method, will return error code 400 in the response.

    Args:
      http_method: HTTP method of request.
      service_path: Service path derived from request URL.
      remote_method: Sub-path after service path has been matched.
    """
    self.response.headers['x-content-type-options'] = 'nosniff'
    if not remote_method and http_method == 'GET':
      # Special case a normal get request, presumably via a browser.
      self.error(405)
      self.__show_info(service_path, remote_method)
      return

    content_type = self.__get_content_type()

    # Provide server state to the service.  If the service object does not have
    # an "initialize_request_state" method, will not attempt to assign state.
    try:
      state_initializer = self.service.initialize_request_state
    except AttributeError:
      pass
    else:
      server_port = self.request.environ.get('SERVER_PORT', None)
      if server_port:
        server_port = int(server_port)

      request_state = remote.HttpRequestState(
          remote_host=self.request.environ.get('REMOTE_HOST', None),
          remote_address=self.request.environ.get('REMOTE_ADDR', None),
          server_host=self.request.environ.get('SERVER_HOST', None),
          server_port=server_port,
          http_method=http_method,
          service_path=service_path,
          headers=list(self.__headers(content_type)))
      state_initializer(request_state)

    if not content_type:
      self.__send_simple_error(400, 'Invalid RPC request: missing content-type')
      return

    # Search for mapper to mediate request.
    for mapper in self.__factory.all_request_mappers():
      if content_type in mapper.content_types:
        break
    else:
      if http_method == 'GET':
        self.error(six.moves.http_client.UNSUPPORTED_MEDIA_TYPE)
        self.__show_info(service_path, remote_method)
      else:
        self.__send_simple_error(six.moves.http_client.UNSUPPORTED_MEDIA_TYPE,
                                 'Unsupported content-type: %s' % content_type)
      return

    try:
      if http_method not in mapper.http_methods:
        if http_method == 'GET':
          self.error(six.moves.http_client.METHOD_NOT_ALLOWED)
          self.__show_info(service_path, remote_method)
        else:
          self.__send_simple_error(six.moves.http_client.METHOD_NOT_ALLOWED,
                                   'Unsupported HTTP method: %s' % http_method)
        return

      try:
        try:
          method = getattr(self.service, remote_method)
          method_info = method.remote
        except AttributeError as err:
          self.__send_error(
          400, remote.RpcState.METHOD_NOT_FOUND_ERROR,
            'Unrecognized RPC method: %s' % remote_method,
            mapper)
          return

        request = mapper.build_request(self, method_info.request_type)
      except (RequestError, messages.DecodeError) as err:
        self.__send_error(400,
                          remote.RpcState.REQUEST_ERROR,
                          'Error parsing ProtoRPC request (%s)' % err,
                          mapper)
        return

      try:
        response = method(request)
      except remote.ApplicationError as err:
        self.__send_error(400,
                          remote.RpcState.APPLICATION_ERROR,
                          unicode(err),
                          mapper,
                          err.error_name)
        return

      mapper.build_response(self, response)
    except Exception as err:
      logging.error('An unexpected error occured when handling RPC: %s',
                    err, exc_info=1)

      self.__send_error(500,
                        remote.RpcState.SERVER_ERROR,
                        'Internal Server Error',
                        mapper)
      return


# TODO(rafek): Support tag-id only forms.
class URLEncodedRPCMapper(RPCMapper):
  """Request mapper for application/x-www-form-urlencoded forms.

  This mapper is useful for building forms that can invoke RPC.  Many services
  are also configured to work using URL encoded request information because
  of its perceived ease of programming and debugging.

  The mapper must be provided with at least method_parameter or
  remote_method_pattern so that it is possible to determine how to determine the
  requests RPC method.  If both are provided, the service will respond to both
  method request types, however, only one may be present in a given request.
  If both types are detected, the request will not match.
  """

  def __init__(self, parameter_prefix=''):
    """Constructor.

    Args:
      parameter_prefix: If provided, all the parameters in the form are
        expected to begin with that prefix.
    """
    # Private attributes:
    #   __parameter_prefix: parameter prefix as provided by constructor
    #     parameter.
    super(URLEncodedRPCMapper, self).__init__(['POST'],
                                              _URLENCODED_CONTENT_TYPE,
                                              self)
    self.__parameter_prefix = parameter_prefix

  def encode_message(self, message):
    """Encode a message using parameter prefix.

    Args:
      message: Message to URL Encode.

    Returns:
      URL encoded message.
    """
    return protourlencode.encode_message(message,
                                         prefix=self.__parameter_prefix)

  @property
  def parameter_prefix(self):
    """Prefix all form parameters are expected to begin with."""
    return self.__parameter_prefix

  def build_request(self, handler, request_type):
    """Build request from URL encoded HTTP request.

    Constructs message from names of URL encoded parameters.  If this service
    handler has a parameter prefix, parameters must begin with it or are
    ignored.

    Args:
      handler: RequestHandler instance that is servicing request.
      request_type: Message type to build.

    Returns:
      Instance of request_type populated by protocol buffer in request
        parameters.

    Raises:
      RequestError if message type contains nested message field or repeated
      message field.  Will raise RequestError if there are any repeated
      parameters.
    """
    request = request_type()
    builder = protourlencode.URLEncodedRequestBuilder(
        request, prefix=self.__parameter_prefix)
    for argument in sorted(handler.request.arguments()):
      values = handler.request.get_all(argument)
      try:
        builder.add_parameter(argument, values)
      except messages.DecodeError as err:
        raise RequestError(str(err))
    return request


class ProtobufRPCMapper(RPCMapper):
  """Request mapper for application/x-protobuf service requests.

  This mapper will parse protocol buffer from a POST body and return the request
  as a protocol buffer.
  """

  def __init__(self):
    super(ProtobufRPCMapper, self).__init__(['POST'],
                                            _PROTOBUF_CONTENT_TYPE,
                                            protobuf)


class JSONRPCMapper(RPCMapper):
  """Request mapper for application/x-protobuf service requests.

  This mapper will parse protocol buffer from a POST body and return the request
  as a protocol buffer.
  """

  def __init__(self):
    super(JSONRPCMapper, self).__init__(
        ['POST'],
        _JSON_CONTENT_TYPE,
        protojson,
        content_types=_EXTRA_JSON_CONTENT_TYPES)


def service_mapping(services,
                    registry_path=DEFAULT_REGISTRY_PATH):
  """Create a services mapping for use with webapp.

  Creates basic default configuration and registration for ProtoRPC services.
  Each service listed in the service mapping has a standard service handler
  factory created for it.

  The list of mappings can either be an explicit path to service mapping or
  just services.  If mappings are just services, they will automatically
  be mapped to their default name.  For exampel:

    package = 'my_package'

    class MyService(remote.Service):
      ...

    server_mapping([('/my_path', MyService),  # Maps to /my_path
                    MyService,                # Maps to /my_package/MyService
                   ])

  Specifying a service mapping:

    Normally services are mapped to URL paths by specifying a tuple
    (path, service):
      path: The path the service resides on.
      service: The service class or service factory for creating new instances
        of the service.  For more information about service factories, please
        see remote.Service.new_factory.

    If no tuple is provided, and therefore no path specified, a default path
    is calculated by using the fully qualified service name using a URL path
    separator for each of its components instead of a '.'.

  Args:
    services: Can be service type, service factory or string definition name of
        service being mapped or list of tuples (path, service):
      path: Path on server to map service to.
      service: Service type, service factory or string definition name of
        service being mapped.
      Can also be a dict.  If so, the keys are treated as the path and values as
      the service.
    registry_path: Path to give to registry service.  Use None to disable
      registry service.

  Returns:
    List of tuples defining a mapping of request handlers compatible with a
    webapp application.

  Raises:
    ServiceConfigurationError when duplicate paths are provided.
  """
  if isinstance(services, dict):
    services = six.iteritems(services)
  mapping = []
  registry_map = {}

  if registry_path is not None:
    registry_service = registry.RegistryService.new_factory(registry_map)
    services = list(services) + [(registry_path, registry_service)]
    mapping.append((registry_path + r'/form(?:/)?',
                    forms.FormsHandler.new_factory(registry_path)))
    mapping.append((registry_path + r'/form/(.+)', forms.ResourceHandler))

  paths = set()
  for service_item in services:
    infer_path = not isinstance(service_item, (list, tuple))
    if infer_path:
      service = service_item
    else:
      service = service_item[1]

    service_class = getattr(service, 'service_class', service)

    if infer_path:
      path = '/' + service_class.definition_name().replace('.', '/')
    else:
      path = service_item[0]

    if path in paths:
      raise ServiceConfigurationError(
        'Path %r is already defined in service mapping' % path.encode('utf-8'))
    else:
      paths.add(path)

    # Create service mapping for webapp.
    new_mapping = ServiceHandlerFactory.default(service).mapping(path)
    mapping.append(new_mapping)

    # Update registry with service class.
    registry_map[path] = service_class

  return mapping


def run_services(services,
                 registry_path=DEFAULT_REGISTRY_PATH):
  """Handle CGI request using service mapping.

  Args:
    Same as service_mapping.
  """
  mappings = service_mapping(services, registry_path=registry_path)
  application = webapp.WSGIApplication(mappings)
  webapp_util.run_wsgi_app(application)
