#!/usr/bin/python
#
# Copyright (C) 2008 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.


"""Provides HTTP functions for gdata.service to use on Google App Engine

AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
   urlfetch API. Set the http_client member of a GDataService object to an
   instance of an AppEngineHttpClient to allow the gdata library to run on
   Google App Engine.

run_on_appengine: Function which will modify an existing GDataService object
   to allow it to run on App Engine. It works by creating a new instance of
   the AppEngineHttpClient and replacing the GDataService object's 
   http_client.

HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a 
    common interface which is used by gdata.service.GDataService. In other 
    words, this module can be used as the gdata service request handler so 
    that all HTTP requests will be performed by the hosting Google App Engine
    server. 
"""


__author__ = 'api.jscudder (Jeff Scudder)'


import StringIO
import atom.service
import atom.http_interface
from google.appengine.api import urlfetch


def run_on_appengine(gdata_service):
  """Modifies a GDataService object to allow it to run on App Engine.

  Args:
    gdata_service: An instance of AtomService, GDataService, or any
        of their subclasses which has an http_client member.
  """
  gdata_service.http_client = AppEngineHttpClient()


class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
  def __init__(self, headers=None):
    self.debug = False
    self.headers = headers or {}

  def request(self, operation, url, data=None, headers=None):
    """Performs an HTTP call to the server, supports GET, POST, PUT, and
    DELETE.

    Usage example, perform and HTTP GET on http://www.google.com/:
      import atom.http
      client = atom.http.HttpClient()
      http_response = client.request('GET', 'http://www.google.com/')

    Args:
      operation: str The HTTP operation to be performed. This is usually one
          of 'GET', 'POST', 'PUT', or 'DELETE'
      data: filestream, list of parts, or other object which can be converted
          to a string. Should be set to None when performing a GET or DELETE.
          If data is a file-like object which can be read, this method will
          read a chunk of 100K bytes at a time and send them.
          If the data is a list of parts to be sent, each part will be
          evaluated and sent.
      url: The full URL to which the request should be sent. Can be a string
          or atom.url.Url.
      headers: dict of strings. HTTP headers which should be sent
          in the request.
    """
    all_headers = self.headers.copy()
    if headers:
      all_headers.update(headers)

    # Construct the full payload.
    # Assume that data is None or a string.
    data_str = data
    if data:
      if isinstance(data, list):
        # If data is a list of different objects, convert them all to strings
        # and join them together.
        converted_parts = [__ConvertDataPart(x) for x in data]
        data_str = ''.join(converted_parts)
      else:
        data_str = __ConvertDataPart(data)

    # If the list of headers does not include a Content-Length, attempt to
    # calculate it based on the data object.
    if data and 'Content-Length' not in all_headers:
      all_headers['Content-Length'] = len(data_str)

    # Set the content type to the default value if none was set.
    if 'Content-Type' not in all_headers:
      all_headers['Content-Type'] = 'application/atom+xml'

    # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
    if operation == 'GET':
      method = urlfetch.GET
    elif operation == 'POST':
      method = urlfetch.POST
    elif operation == 'PUT':
      method = urlfetch.PUT
    elif operation == 'DELETE':
      method = urlfetch.DELETE
    else:
      method = None
    return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
        method=method, headers=all_headers))
 

def HttpRequest(service, operation, data, uri, extra_headers=None,
    url_params=None, escape_params=True, content_type='application/atom+xml'):
  """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE.

  This function is deprecated, use AppEngineHttpClient.request instead.

  To use this module with gdata.service, you can set this module to be the
  http_request_handler so that HTTP requests use Google App Engine's urlfetch.
  import gdata.service
  import gdata.urlfetch
  gdata.service.http_request_handler = gdata.urlfetch

  Args:
    service: atom.AtomService object which contains some of the parameters
        needed to make the request. The following members are used to
        construct the HTTP call: server (str), additional_headers (dict),
        port (int), and ssl (bool).
    operation: str The HTTP operation to be performed. This is usually one of
        'GET', 'POST', 'PUT', or 'DELETE'
    data: filestream, list of parts, or other object which can be
        converted to a string.
        Should be set to None when performing a GET or PUT.
        If data is a file-like object which can be read, this method will read
        a chunk of 100K bytes at a time and send them.
        If the data is a list of parts to be sent, each part will be evaluated
        and sent.
    uri: The beginning of the URL to which the request should be sent.
        Examples: '/', '/base/feeds/snippets',
        '/m8/feeds/contacts/default/base'
    extra_headers: dict of strings. HTTP headers which should be sent
        in the request. These headers are in addition to those stored in
        service.additional_headers.
    url_params: dict of strings. Key value pairs to be added to the URL as
        URL parameters. For example {'foo':'bar', 'test':'param'} will
        become ?foo=bar&test=param.
    escape_params: bool default True. If true, the keys and values in
        url_params will be URL escaped when the form is constructed
        (Special characters converted to %XX form.)
    content_type: str The MIME type for the data being sent. Defaults to
        'application/atom+xml', this is only used if data is set.
  """
  full_uri = atom.service.BuildUri(uri, url_params, escape_params)
  (server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri)
  # Construct the full URL for the request.
  if ssl:
    full_url = 'https://%s%s' % (server, partial_uri)
  else:
    full_url = 'http://%s%s' % (server, partial_uri)

  # Construct the full payload. 
  # Assume that data is None or a string.
  data_str = data
  if data:
    if isinstance(data, list):
      # If data is a list of different objects, convert them all to strings
      # and join them together.
      converted_parts = [__ConvertDataPart(x) for x in data]
      data_str = ''.join(converted_parts)
    else:
      data_str = __ConvertDataPart(data)

  # Construct the dictionary of HTTP headers.
  headers = {}
  if isinstance(service.additional_headers, dict):
    headers = service.additional_headers.copy()
  if isinstance(extra_headers, dict):
    for header, value in extra_headers.iteritems():
      headers[header] = value
  # Add the content type header (we don't need to calculate content length,
  # since urlfetch.Fetch will calculate for us).
  if content_type:
    headers['Content-Type'] = content_type

  # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
  if operation == 'GET':
    method = urlfetch.GET
  elif operation == 'POST':
    method = urlfetch.POST
  elif operation == 'PUT':
    method = urlfetch.PUT
  elif operation == 'DELETE':
    method = urlfetch.DELETE
  else:
    method = None
  return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str, 
      method=method, headers=headers))


def __ConvertDataPart(data):
  if not data or isinstance(data, str):
    return data
  elif hasattr(data, 'read'):
    # data is a file like object, so read it completely.
    return data.read()
  # The data object was not a file.
  # Try to convert to a string and send the data.
  return str(data)


class HttpResponse(object):
  """Translates a urlfetch resoinse to look like an hhtplib resoinse.
  
  Used to allow the resoinse from HttpRequest to be usable by gdata.service
  methods.
  """

  def __init__(self, urlfetch_response):
    self.body = StringIO.StringIO(urlfetch_response.content)
    self.headers = urlfetch_response.headers
    self.status = urlfetch_response.status_code
    self.reason = ''

  def read(self, length=None):
    if not length:
      return self.body.read()
    else:
      return self.body.read(length)

  def getheader(self, name):
    if not self.headers.has_key(name):
      return self.headers[name.lower()]
    return self.headers[name]
    
