#!/usr/bin/python
# coding: iso-8859-15

global asyncore
import asyncore
global socket
import socket
global urlparse
import urlparse
global cStringIO
import cStringIO
#//LIB_START
global sys
import sys
global time
import time
global string
import string
global hsl20_4
from hsl20_4 import *
#//LIB_END

## @brief Klassen zum Abrufen von Daten per HTTP
## @deprecated Diese Klasse sollte nicht mehr eingesetzt werden. Im Beispiel @e https_client @e WebRequest (<i>10707_WebRequest (https).py</i>) wird gezeigt, wie sie durch Standardmodule von Python ersetzt werden kann.
## Klasse ist deprecated @n
## Falsch benannte Attribute @ GET und @e POST korrigiert. (Waren: @e METHOD_GET und @e METHOD_POST)
class hsl20_4_http_client:

    ## GET
    GET = "GET"

    ## POST
    POST ="POST"

    ## PUT
    PUT ="PUT"

    ## DELETE
    DELETE = "DELETE"

    ## TIMEOUT
    TIMEOUT = 15.0


    ## HTTP-Client-Response
    ##
	## @deprecated Diese Klasse sollte nicht mehr eingesetzt werden. Im Beispiel @e https_client @e WebRequest wird gezeigt, wie sie durch Standardmodule von Python ersetzt werden kann.
	##
    ## In dieser Klasse werden alle Parameter gekapselt, die die Gegenstelle auf die Anfrage geantwortet hat.
    ## @chg20_03 Klasse ist deprecated @n
    ## Hinweis auf das neue Beispiel hinzugefügt
    ## @if OLD_CHANGELOG
    ## @chlg04 neue Klasse
    ## @endif
    class Response:

        ## Konstruktor
        ##
        ## @warning Diese Klasse sollte nicht direkt instanziert werden.
        def __init__(self, stream):
            ## @cond NO_DOC
            self.__rfile = stream
            self.__header = {}
            self.__body = None
            self.__status_code = 0
            self.__status_text = ""
            self.__http_version = None
            ## @endcond


        ## @cond NO_DOC
        def _clear(self):
            self.__rfile = None


        def _free(self):
            self.__rfile = None
            self.__header = None
            self.__body = None


        def _parse(self):
            if self.__rfile == None:
                return

            self.__rfile.seek(0)

            header_lines = []

            # header ermitteln
            line = self.__rfile.readline()
            while line!="\r\n" and line!="":
                header_lines.append(line)
                line = self.__rfile.readline()

            if len(header_lines)>=1:
                if string.upper(header_lines[0][:4])=="HTTP":
                    t = string.split(string.strip(header_lines[0]), " ", 2)
                    self.__http_version = t[0]
                    self.__status_code = int(t[1])
                    self.__status_text = t[2]
                lastline = ''
                key=None
                value=None
                for k in range(len(header_lines)-1):
                    line = header_lines[k+1]
                    if line[0]==" " or line[0]=="\t":
                        if key!=None:
                            self.__header[string.lower(key)]+=string.rstrip(line)
                    else:
                        idx = line.find(':')
                        key = line[:idx]
                        value = string.strip(line[idx+1:])
                        self.__header[string.lower(key)] = value

            # body
            self.__body = self.__rfile.read()
        ## @endcond


        ## Liefert den Versions-String der Anfrage zurück (z.B: HTTP/1.1)
        ## @result @e string @n Protokollversion. Liefert @e None, wenn der Server nicht HTTP konform geantwortet hat.
        def get_http_version(self):
            return self.__http_version


        ## Liefert den HTTP-Status-Code der Anfrage zurück (z.B: 200).
        ## @result @e int @n HTTP-Statuscode. Liefert 0, wenn der Server nicht HTTP konform geantwortet hat.
        def get_status_code(self):
            return self.__status_code


        ## Liefert den HTTP-Statustext der Anfrage zurück (z.B: OK).
        ## @result @e string @n Statustext. Liefert "", wenn der Server nicht HTTP konform geantwortet hat.
        def get_status_text(self):
            return self.__status_text


        ## Liefert die von der Gegenstelle übermittelten Header als Dictionary zurück.
        ## @note Zu Beachten: Die Schlüssel werden komplett in Kleinbuchstaben zurückgegeben!
        ## @result @e dictionary @n Alle Header in Form eines Dictionaries
        def get_headers(self):
            return self.__header


        ## Liefert den Wert eines Header-Eintrags.
        ## @param key @e string @n Schlüssel
        ## @result @e string @n Wert. Liefert None, falls ein ungültiger Schlüssel übergeben wurde.
        def get_header(self, key):
            ## @cond NO_DOC
            if self.__header.has_key(string.lower(key)):
                return self.__header[string.lower(key)]
            else:
                return None
            ## @endcond

        ## Liefert den Body-Teil der Antwort zurück.
        ## @result @e string @n Body
        def get_body(self):
            return self.__body



    ## @brief Alle Methoden des HTTP-Clients.
	## @deprecated Diese Klasse sollte nicht mehr eingesetzt werden. Im Beispiel @e https_client @e WebRequest wird gezeigt, wie sie durch Standardmodule von Python ersetzt werden kann.
    ## @details
    ## Dient zur Kommunikation mit einer Gegenstelle per HTTP.
    ##
    ## Timeouts
    ## --
    ## Folgende Timeouts, bei deren Eintreten die Verbindung getrennt wird, sind zu beachten:
    ## - Der Zeitraum zwischen zwei empfangenen Paketen ist größer als 15 Sekunden
    ## - Seit Aufbau der Verbindung sind mehr als 30 Sekunden vergangen und der Header wurde noch nicht korrekt gesendet
    ##
    ## @code
    ## class mein_demo_modul(hsl20_4.BaseModule):
    ##
    ##   ...
    ##
    ##   def get_demo_page(self):
    ##     client = self.FRAMEWORK.create_http_client()
    ##     client.set_url("GET", "http://www.example.net/")
    ##     client.set_on_data(self.on_example_data)
    ##     client.start_request()
    ##
    ##   def on_example_data(self, http_response):
    ##     if http_response.get_status_code()==200:
    ##       # Antwort auswerten
    ##
    ## @endcode
    ## @note Die Klasse arbeitet asynchron. Keine der angebotenen Methoden blockiert.
    class Client:

        ## Konstruktor
        ##
        ## @warning Diese Klasse sollte nicht direkt instanziert werden.
        def __init__(self, framework, context_map):
            ## @cond NO_DOC
            self._framework = framework
            self._context_map = context_map
            self._data_callback = None
            self._error_callback = None

            self.__method = "GET"
            self.__http_version = 1
            self.__header = {}
            self.__body = None
            self.__stream = cStringIO.StringIO()

            self._host = None
            self._port = 80
            self._username = None
            self._password = None
            self._path = None

            self.__client=None
            self.__sent = False
            self.__timeout = 30
            ## @endcond


        ## Setzt die URL, die aufgerufen werden soll.
        ## @param method @e string @n Abruf-Methode, die für die URL verwendet werden soll (z.B. GET).
        ## @param url @e string @n URL (z.B. http://www.example.net/)
        ## @param http_version @e int @n Optional. HTTP-Version. 
        ## - 1 => HTTP/1.0 (default)
        ## - 2 => HTTP/1.1
        ## @note Die übergebende URL muss korrekt kodiert sein.
        ## @exception ValueError @n Wird ausgelöst, wenn eine ungültige URL übergeben wurde.
        ## @exception ValueError @n Wird ausgelöst, wenn eine ungültige HTTP-Version übergeben wurde.
        ## @if OLD_CHANGELOG
        ## @chlg04 Neuer Parameter: @e http_version @n
        ## Neue Exceptions: ValueError bei ungültiger @e url und/oder ungültiger @e http_version
        ## @endif
        def set_url(self, method, url, http_version=1):
            ## @cond NO_DOC
            if http_version!=1 and http_version!=2:
                raise ValueError('invalid http_version')

            o = urlparse.urlsplit(url)
            if o[0]=='http':
                self._host = o.hostname
                self._port = o.port
                if self._port==None:
                    self._port = 80
                self._username = o.username
                self._password = o.password
                if len(o[2])==0:
                    p = "/"
                else:
                    p = o[2]
                if len(o[3])>0:
                    self._path = p + '?' + o[3]
                else:
                    self._path = p
                self.__http_version = http_version
                self.__header['Host'] = o.hostname
                self.__method = string.upper(method)
            else:
                raise ValueError('http scheme not found')
            ## @endcond



        ## Setzt einen Header. Zum Beispiel: `set_header("Content-type", "text/html")` @n
        ## Es können mehrere Header definiert werden.
        ## @param key @e string @n Schlüssel des Headers
        ## @param value @e string @n Wert
        ## @if OLD_CHANGELOG
        ## @chlg04 Beispiel für Standard-Aufruf hinzugefügt
        ## @endif
        def set_header(self, key, value):
            ## @cond NO_DOC
            self.__header[key] = value
            ## @endcond


        ## Setzt den Body. Wird nur bei z.B. POST und GET benötigt. Die Methode setzt
        ## außerdem den HTTP-Header "Content-length" auf Basis der unter @e data übergebenen Daten.
        ## @param data @e string @n Daten
        ## @param content_type @e string @n Optional, setzt im Header den Content-Type der zu übermittelnden Daten (default: @e None)
        ## @chlg19 Hinweis zum default-Wert von Parameter @e content_type hinzugefügt
        def set_body(self, data, content_type=None):
            ## @cond NO_DOC
            if content_type!=None:
                self.__header["Content-type"] = content_type
            if (data!=None) and (len(data)>=0):
                self.__header["Content-length"] = len(data)
                self.__body = data
            ## @endcond


        ## Legt eine Callback-Methode für die Antwort fest. Die Methode wird
        ## aufgerufen, wenn die Gegenstelle die Anfrage vollständig beantwortet hat,
        ## oder wenn ein Fehler aufgetreten ist.
        ##
        ## Die Callback-Methode benötigt folgende Parameter:
        ## - response @e hsl20_4_http_client.hsl20_4_http_client.Response @n Response-Objekt. Enthält die Antwort-Daten der Gegenstelle.
        ##
        ## @param callback @e function @n Callback-Methode
        ## @if OLD_CHANGELOG
        ## @chlg04 Parameter für Callback geändert
        ## @endif
        def set_on_data(self, callback):
            ## @cond NO_DOC
            self._data_callback = callback
            ## @endcond


        ## Legt eine Callback-Methode für Fehler fest. Die Callback-Methode wird aufgerufen,
        ## wenn während der Kommunikation ein Fehler oder Timeout aufgetreten ist.
        ##
        ## Die Callback-Methode benötigt folgende Parameter:
        ## - exception @e exception @n Die aufgetretene Exception. Liefert @e None, wenn ein unbekannter Fehler aufgetreten ist.
        ##
        ## @param callback @e function @n Callback-Methode
        ## @if OLD_CHANGELOG
        ## @chlg04 Neue Methode
        ## @endif
        def set_on_error(self, callback):
            ## @cond NO_DOC
            self._error_callback = callback
            ## @endcond


        ## Startet den HTTP-Abruf.
        ##
        ## Löst eine Exception aus, wenn nicht alle für den Abruf benötigten Attribute gesetzt wurden.
        ## @exception AttributeError @n Löst eine Exception aus, wenn nicht alle für den Abruf benötigten Attribute gesetzt wurden.
        ## @exception RuntimeError @n Löst eine Exception aus, wenn der Hostname nicht aufgelöst werden konnte.
        ## @exception RuntimeError @n Löst eine Exception aus, wenn die Methode bereits aufgerufen wurde.
        ## @if OLD_CHANGELOG
        ## @chlg18 Fehler bei der Notation der Exceptions korrigiert.
        ## @endif
        def start_request(self):
            ## @cond NO_DOC
            if self.__method==None:
                raise AttributeError("no method")
            if self._path==None:
                raise AttributeError("no url")
            if self.__sent:
                raise RuntimeError('http-client already running')

            ip = self._framework.resolve_dns(self._host)
            if ip==None:
                raise RuntimeError("can't resolve host")

            if self.__http_version==2:
                hv = 'HTTP/1.1'
            else:
                hv = 'HTTP/1.0'
            lines = ['%s %s %s' % (self.__method, self._path, hv)]
            self.__header["Connection"] = "close"
            for hdr in self.__header:
                lines.append('%s: %s' % (hdr, self.__header[hdr]))
            lines.append('')
            # folgende zeile auskommentieren um timeout zu simulieren
            lines.append('')

            self.__stream = cStringIO.StringIO()
            self.__stream.write(string.join(lines, '\r\n'))
            if self.__body!=None:
                if type(self.__body) is str:
                    self.__stream.write(self.__body)
                else:
                    self.__stream.write(str(self.__body))

            self.__sent = True
            self.__client = hsl20_4_http_client._HttpClient(self)
            self.__client.set_stream(self.__stream)
            self.__client.start( (ip, self._port) )
            self._framework._signal_asyncore_select_interrupt()
            ## @endcond


        ## @cond NO_DOC
        # Im Kontext des Moduls aufrufen
        def _finish(self):
            # Gesendete Daten freigeben
            if self.__stream!=None:
                try:
                    self.__stream.close()
                except:
                    pass
                self.__stream = None

            error = False
            exception = None
            http = None
            if (self.__client!=None):
                if self.__client.has_error():
                    error = True
                    exception = self.__client.get_last_exception()
                else:
                    data=self.__client.get_data()
                    if (data==None):
                        error = True
                        exception = Exception('no data')
                    else:
                        http = hsl20_4_http_client.Response(data)
                        http._parse()
                        http._clear()
                        if http.get_status_code()==0:
                            error = True
                            exception = Exception('no response')

            else:
                error = True

            if error:
                # Fehler aufgetreten
                if (self._error_callback!=None):
                    try:
                        if exception==None:
                            exception = Exception('unknown error')

                        self._error_callback(exception)
                    except:
                        hsl20_4.Framework._get_global_debug_section().add_exception()
            else:
                # Daten ok
                if (self._data_callback!=None):
                    try:
                        self._data_callback(http)
                    except:
                        hsl20_4.Framework._get_global_debug_section().add_exception()
                    if http!=None:
                        http._free()

            # Daten freigeben
            try:
                del exception
            except:
                pass
            if self.__client!=None:
                try:
                    self.__client.clear()
                except:
                    pass
                self.__client = None
        ## @endcond


    ## @cond NO_DOC
    ## Intern  
    ## Kapselt den Asyncore-Dispatcher und empfängt eine HTTP-Anfrage.
    class _HttpClient(asyncore.dispatcher):

        def __init__(self, parent, logger=None):
            ## Parameter setzen und Konstruktor aufrufen
            asyncore.dispatcher.__init__(self, map=parent._context_map)
            self.__parent = parent
            self.__logger = None

            ## Daten die auf den Socket geschrieben werden
            self.__rfile = None
            ## Daten die vom Socket gelesen werden
            self.__wfile = cStringIO.StringIO()

            ## Lokale Attribute initialisieren
            self.__created = time.time()
            self.__last_ts = self.__created
            self.__closed = False
            self.__finish = False
            self.__error = False
            self.__exception = None


        # Alle Referenzen von außen loeschen
        def clear(self):
            self.__logger = None
            self.__exception = None
            try:
                self.__wfile.close()
            except:
                pass
            self.__wfile = None

        def set_stream(self, stream):
            self.__rfile = stream
            self.__rfile.seek(0)


        def get_data(self):
            if self.__wfile!=None and not self.__wfile.closed:
                return self.__wfile
            else:
                return None


        def has_error(self):
            return self.__error


        def get_last_exception(self):
            return self.__exception


        def start(self, address):
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect( address )


        def log_message(self, format, *args):
            s = format % args
            if self.__logger!=None:
                self.__logger.info("http-client log: " + s)


        def handle_connect(self):
            pass


        ## Alle "inneren" Referenzen freigeben
        def __free(self):
            self.__finish = True
            self.__parent = None


        def __is_free(self):
            return (self.__finish) or (self.__parent==None) or (self.__error)


        def handle_error(self):
            if (self.__is_free()):
                return

            self.__error = True
            try:
                self.__exception = sys.exc_info()[1]
            except:
                self.__exception = None
            self.__parent._framework._run_in_context_thread(self.__parent._finish)
            if (self.__parent._error_callback==None):
                hsl20_4.Framework._get_global_debug_section().add_exception()
            self.__free()
            self.handle_close()


        def handle_expt(self):
            if (self.__is_free()):
                return

            self.__error = True
            self.__exception = None
            self.__parent._framework._run_in_context_thread(self.__parent._finish)
            if (self.__parent._error_callback==None):
                try:
                    raise Exception('socket exception in http client')
                except:
                    hsl20_4.Framework._get_global_debug_section().add_exception()
            self.__free()
            self.handle_close()


        def handle_close(self):
            if not self.__closed:
                self.__closed = True
                self.close()

            if self.__is_free():
                return

            self.__parent._framework._run_in_context_thread(self.__parent._finish)
            self.__free()


        def handle_read(self):
            data = self.recv(8192)
            self.__last_ts = time.time()
            if data and (self.__wfile!=None) and (not self.__wfile.closed):
                self.__wfile.write(data)


        def readable(self):
            ts = time.time()
            if (ts-self.__last_ts>hsl20_4_http_client.TIMEOUT):
                try:
                    raise Exception('timeout')
                except:
                    self.handle_error()
                self.handle_close()
                return False
            return (not self.__finish)


        def writable(self):
            return (not self.__closed) and (self.__rfile!=None) and (not self.__rfile.closed)


        def handle_write(self):
            data = self.__rfile.read(8192)
            if data!="":
                sent = self.send(data)
            else:
                try:
                    self.__rfile.close()
                except:
                    pass
                self.__rfile = None

    ## @endcond
