##############################################################################
#
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
#                    Vincent Pelletier <vincent@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

import SOAPpy
from Products.AGProjects.patches import SOAPpy_WSDL as WSDL
from AccessControl.SecurityInfo import allow_class
import threading

# Exception class.
#  This allows restricted python to handle exceptions without allowing direct
#  import of SOAPpy module (because it should not have to be dependant from
#  underlying interface API, only this file has to be).

class SOAPWSDLException(Exception):

  __allow_access_to_unprotected_subobjects__ = 1

  def __init__(self, code, name, info): # pylint: disable=super-init-not-called
    self.code = code
    self.name = name
    self.info = info

  def getCode(self):
    return self.code

  def getName(self):
    return self.name

  def getInfo(self):
    return self.info

  def __str__(self):
    return '<%s at 0x%x: %r %r %r>' % (self.__class__.__name__, id(self),
       self.name, self.code, self.info)

allow_class(SOAPWSDLException)

# Authentication classes.
#  These are SOAP authentication classes.
#  They are supposed to be instanciated and then transmited to WebServiceTool
#  in order to create a connection.

class AuthenticationBase(object):
  """
    Authentication API.

    As SOAP doens't provide a standard authentication method, authentication
    plugins must be written for virtualy each SOAP services.
    This API intends to provide hooks for authentication purposes.

    Overload the methods you need (default methods are NO-OPs).
  """

  def ProxyParameterDictHook(self):
    """
      Return a dictionary of extra parameters to provide to WSDL.Proxy .
    """
    return {}

  def AfterConnectionHook(self, connection):
    """
      This hook is called upon connection. It can be used to exchange
      credentials with remote server.
    """
    pass

class NullAuthentication(AuthenticationBase):
  """
    NO-OP authentication.
  """
  pass

class HeaderAuthentication(AuthenticationBase):
  """
    Authentication implementation for authentication mechanism based on known
    credentials. Those credentials are put in SOAP packet header, in "auth"
    XML block.
  """
  def __init__(self, auth):
    self._auth = auth

  def ProxyParameterDictHook(self):
    return {'auth': self._auth}

allow_class(HeaderAuthentication)

# Wrappers for SOAPpy objects.
#  These wrappers will be returned by the connector, and can be used in
#  restricted scripts.

class WSDLConnection(object):

  __allow_access_to_unprotected_subobjects__ = 1

  def __init__(self, wsdl, credentials, service):
    self._wsdl = wsdl
    self._credentials = credentials
    self._service = service
    self._port_dict = {}

  def __getitem__(self, port_name):
    """
      Connect to requested port.
    """
    try:
      result = self._port_dict[port_name]
    except KeyError:
      kw = self._credentials.ProxyParameterDictHook()
      result = self._port_dict[port_name] = PortWrapper(WSDL.Proxy(
        self._wsdl, service=self._service, port=port_name, **kw))
      self._credentials.AfterConnectionHook(result)
    return result

class PortWrapper(object):

  __allow_access_to_unprotected_subobjects__ = 1

  def __init__(self, port):
    self._port = port

  def __getattr__(self, method_id):
    return MethodWrapper(getattr(self._port, method_id))

class MethodWrapper(object):

  __allow_access_to_unprotected_subobjects__ = 1

  def __init__(self, method):
    self._method = method

  def __call__(self, *args, **kw):
    try:
      return self._method(*args, **kw)
    except SOAPpy.Types.faultType, exception:
      raise SOAPWSDLException(*exception())

# SOAPpy says nothing about thread-safeness of parsed WSDL.
# Be on the safe side by using threading.local as a storage for it.
wsdl_cache = threading.local()

# XXX: SOAPpy.wstools.WSDLTools.WSDL.__del__ calls unlink on an xml document
# instance, which happens to fail (AttributeError: NoneType has no attribute
# 'unlink') somewhere down in xml module. As that unlink is only acting on xml
# nodes in memory, it's safe to ignore it.
def WSDL___del__(self):
  if self.document is not None:
    unlink = self.document.unlink
    try:
      unlink()
    except AttributeError:
      pass
SOAPpy.wstools.WSDLTools.WSDL.__del__ = WSDL___del__

class SOAPWSDLConnection:
  """
    Holds a SOAP connection described by a WSDL file.
    This uses a patch from NGNPro over SOAPpy's WSDL.py file, allowing the
    WSDL to describe multiple ports and adding support for authentication by
    header.

    Note: SOAP doesn't describe a standard way to handle authentication, so it
    might not fit your needs.
  """

  def __init__(self, url, user_name=None, password=None, credentials=None,
               service=None):
    """
      url (string)
        The url of the WSDL file describing an underlying SOAP interface.
      user_name (string or None)
      password (string is None)
        The transport-level (http) credentials to use.
      credentials (AuthenticationBase subclass instance or None)
        The interface-level (SOAP) credentials to use.
      service (string or None)
        The WSDL-described service to connect to.
    """
    self.url = url
    self._user_name = user_name
    self._password = password
    if credentials is None:
      credentials = NullAuthentication()
    self._credentials = credentials
    self._service = service

  def connect(self):
    try:
      wsdl = wsdl_cache.parsed
    except AttributeError:
      wsdl = wsdl_cache.parsed = SOAPpy.wstools.WSDLTools.WSDLReader().loadFromURL(self.url)
    # TODO: transport (http) level authentication using self._user_name and
    # self._password
    return WSDLConnection(wsdl, self._credentials, self._service)