Imports System.Xml
Imports System.Net
Imports System.IO
Imports System.Web
Imports System.Text.RegularExpressions
Imports tsl5.Extensiones.BinaryReaderExtensions

''' <summary>
''' Esta clase es una alternativa para cuando no puedes usar un cliente WCF (Referencia de Servicio) o la interfaz generada por wdsl.exe de .Net Framework 2.0.
''' Permite invocar métodos de un servicio web conociendo la URL del "endpoint" del servicio web, pero con la pega de que los mensajes que se envían para
''' invocar los servicios deben ser generados manualmente.
''' </summary>
Public Class ClienteServicioWeb
    Public Property Url() As String
        Get
            Return m_Url
        End Get
        Set(value As String)
            m_Url = value
        End Set
    End Property
    Private m_Url As String
    Public Property Method() As String
        Get
            Return m_Method
        End Get
        Private Set(value As String)
            m_Method = value
        End Set
    End Property
    Private m_Method As String
    Private RequestString As String = [String].Empty
    Private Username As String = [String].Empty
    Private Password As String = [String].Empty
    Private sAuth As String = [String].Empty
    Public Params As New Dictionary(Of String, String)()
    Public ResponseSOAP As XDocument = XDocument.Parse("<root/>")
    Public ResultXML As XDocument = XDocument.Parse("<root/>")
    Public ResultString As String = [String].Empty

    Public Sub New()
        Url = [String].Empty
        Method = [String].Empty
    End Sub
    Public Sub New(baseUrl As String)
        Url = baseUrl
        Method = [String].Empty
    End Sub
    Public Sub New(baseUrl As String, methodName As String)
        Url = baseUrl
        Method = methodName
    End Sub

#Region "Métodos públicos"
    ''' <summary>
    ''' Añade un parámetro a la llamada al método del servicio web.
    ''' </summary>
    ''' <param name="name">Nombre del parámetro (sensible a mayúsculas).</param>
    ''' <param name="value">Valor del parámetro.</param>
    ''' <remarks>Intermanente estos parámetros se mandan como parámetros POST.</remarks>
    Public Sub AddParameter(name As String, value As String)
        Params.Add(name, value)
    End Sub
    ''' <summary>
    ''' Añade credenciales para autenticarse en el servicio web usando autenticación HTTP básica.
    ''' </summary>
    ''' <param name="username"></param>
    ''' <param name="password"></param>
    ''' <remarks>Se usa codificación UTF-8 para transmitir estas credenciales.</remarks>
    Public Sub AddBasicAuthenticationCredential(ByVal username As String, ByVal password As String)
        Me.sAuth = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(username & ":" + password))
        Me.Username = username
        Me.Password = password
    End Sub
    Public Sub SetRequestString(ByVal sRequest As String)
        Me.RequestString = sRequest
    End Sub
    Public Sub Invoke(Optional ByVal ignoreSSLErrors As Boolean = False)
        Invoke(Method, True, ignoreSSLErrors = ignoreSSLErrors)
    End Sub
    ''' <summary>
    ''' Invoca un método del servicio web, identificado por su nombre.
    ''' </summary>
    ''' <param name="methodName">Nombre del método del servicio web.</param>
    Public Sub Invoke(methodName As String,
        Optional ByVal ignoreSSLErrors As Boolean = False)

        Invoke(methodName, True, ignoreSSLErrors = ignoreSSLErrors)
    End Sub
    ''' <summary>
    ''' Limpia todos los datos del objeto excepto la URL del endpoint del servicio web.
    ''' Es útil para realizar subsecuentes llamadas al mismo servicio web, con otros datos o invocando a otros métodos.
    ''' </summary>
    Public Sub CleanLastInvoke()
        ResponseSOAP = InlineAssignHelper(ResultXML, Nothing)
        ResultString = InlineAssignHelper(Method, [String].Empty)
        Params = New Dictionary(Of String, String)()
    End Sub
#End Region
#Region "Métodos auxiliares públicos"
    ''' <summary>
    ''' Remove all xmlns:* instances from the passed XmlDocument to simplify our xpath expressions
    ''' </summary>
    Public Shared Function RemoveNamespaces(oldXml As XDocument) As XDocument
        ' FROM: http://social.msdn.microsoft.com/Forums/en-US/bed57335-827a-4731-b6da-a7636ac29f21/xdocument-remove-namespace?forum=linqprojectgeneral
        Try
            Dim newXml As XDocument = XDocument.Parse(Regex.Replace(oldXml.ToString(), "(xmlns:?[^=]*=[""][^""]*[""])", "", RegexOptions.IgnoreCase Or RegexOptions.Multiline))
            Return newXml
        Catch [error] As XmlException
            Throw New XmlException([error].Message + " at WSCUtils.RemoveNamespaces")
        End Try
    End Function
    ''' <summary>
    ''' Remove all xmlns:* instances from the passed XmlDocument to simplify our xpath expressions
    ''' </summary>
    Public Shared Function RemoveNamespaces(oldXml As String) As XDocument
        Dim newXml As XDocument = XDocument.Parse(oldXml)
        Return RemoveNamespaces(newXml)
    End Function
    ''' <summary>
    ''' Elimina todos los espacios de nombres de un documento XML
    ''' </summary>
    Public Shared Function EliminarEspaciosDeNombres(xDocumento As XDocument) As XDocument
        ' FROM: http://social.msdn.microsoft.com/Forums/en-US/bed57335-827a-4731-b6da-a7636ac29f21/xdocument-remove-namespace?forum=linqprojectgeneral
        Try
            Dim sRespuestaSinNamespaces As String = System.Text.RegularExpressions.Regex.Replace(xDocumento.ToString(),
                                   "(xmlns:?[^=]*=[""][^""]*[""])", "",
                                    System.Text.RegularExpressions.RegexOptions.IgnoreCase Or System.Text.RegularExpressions.RegexOptions.Multiline)
            sRespuestaSinNamespaces = System.Text.RegularExpressions.Regex.Replace(sRespuestaSinNamespaces,
                                      "<\w+:", "<",
                                       System.Text.RegularExpressions.RegexOptions.IgnoreCase Or System.Text.RegularExpressions.RegexOptions.Multiline)
            sRespuestaSinNamespaces = System.Text.RegularExpressions.Regex.Replace(sRespuestaSinNamespaces,
                                      "</\w+:", "</",
                                       System.Text.RegularExpressions.RegexOptions.IgnoreCase Or System.Text.RegularExpressions.RegexOptions.Multiline)
            Return XDocument.Parse(sRespuestaSinNamespaces)
        Catch [error] As XmlException
            Throw New XmlException([error].Message + " at WSCUtils.EliminarEspaciosDeNombres")
        End Try
    End Function
    ''' <summary>
    ''' Converts a string that has been HTML-enconded for HTTP transmission into a decoded string.
    ''' </summary>
    ''' <param name="escapedString">String to decode.</param>
    ''' <returns>Decoded (unescaped) string.</returns>
    Public Shared Function UnescapeString(escapedString As String) As String
        Return HttpUtility.HtmlDecode(escapedString)
    End Function
#End Region
#Region "Métodos auxiliares privados"
    Private Function GetCredential() As CredentialCache
        'ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3
        Dim credentialCache As New CredentialCache()
        credentialCache.Add(New System.Uri(Me.Url), "Basic", New NetworkCredential(Me.Username, Me.Password))
        Return credentialCache
    End Function
    ''' <summary>
    ''' Checks if the WebService's URL and the WebMethod's name are valid. If not, throws ArgumentNullException.
    ''' </summary>
    ''' <param name="methodName">Web Method name (optional)</param>
    Private Sub AssertCanInvoke(Optional methodName As String = "")
        If Url = [String].Empty Then
            Throw New ArgumentNullException("You tried to invoke a webservice without specifying the WebService's URL.")
        End If
        If (methodName = "") AndAlso (Method = [String].Empty) Then
            Throw New ArgumentNullException("You tried to invoke a webservice without specifying the WebMethod.")
        End If
    End Sub
    ''' <summary>
    ''' Invokes a Web Method, with its parameters encoded or not.
    ''' </summary>
    ''' <param name="methodName">Name of the web method you want to call (case sensitive)</param>
    ''' <param name="encode">Do you want to encode your parameters? (default: true)</param>
    Private Function Invoke(methodName As String, encode As Boolean,
        Optional ByVal ignoreSSLErrors As Boolean = False) As String

        AssertCanInvoke(methodName)
        Dim soapStr As String = "<?xml version=""1.0"" encoding=""utf-8""?>" & vbCr & vbLf & "                <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""" & vbCr & vbLf & "                   xmlns:xsd=""http://www.w3.org/2001/XMLSchema""" & vbCr & vbLf & "                   xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" & vbCr & vbLf & "                  <soap:Body>" & vbCr & vbLf & "                    <{0} xmlns=""http://tempuri.org/"">" & vbCr & vbLf & "                      {1}" & vbCr & vbLf & "                    </{0}>" & vbCr & vbLf & "                  </soap:Body>" & vbCr & vbLf & "                </soap:Envelope>"

        Dim req As HttpWebRequest = DirectCast(WebRequest.Create(Url), HttpWebRequest)

        If ignoreSSLErrors Then
            req.ServerCertificateValidationCallback() = Function(sender, certificate, chain, sslPolicyErrors) True
        End If

        req.Headers.Add("SOAPAction", (Convert.ToString("""http://tempuri.org/") & methodName) + """")
        req.ContentType = "text/xml;charset=""utf-8"""
        req.Accept = "text/xml"
        req.Method = "POST"

        'If Not String.IsNullOrWhiteSpace(sAuth) Then
        '    req.Headers.Add("Authorization", "Basic " + sAuth)
        'End If

        If Not String.IsNullOrWhiteSpace(Me.Username) AndAlso Not String.IsNullOrWhiteSpace(Me.Password) Then
            req.Credentials = GetCredential()
            req.PreAuthenticate = True
        End If

        Using stm As Stream = req.GetRequestStream()
            Dim postValues As String = ""
            For Each param In Params
                If encode Then
                    postValues += String.Format("<{0}>{1}</{0}>", HttpUtility.UrlEncode(param.Key), HttpUtility.UrlEncode(param.Value))
                Else
                    postValues += String.Format("<{0}>{1}</{0}>", param.Key, param.Value)
                End If
            Next

            soapStr = String.Format(soapStr, methodName, postValues)
            Using stmw As New StreamWriter(stm)
                stmw.Write(soapStr)
            End Using
        End Using

        Using responseReader As New StreamReader(req.GetResponse().GetResponseStream())
            Dim result As String = responseReader.ReadToEnd()
            ResponseSOAP = XDocument.Parse(UnescapeString(result))
        End Using

        Me.ResultString = ResponseSOAP.ToString
        Me.ResultXML = ResponseSOAP

        Return ResponseSOAP.ToString
    End Function
    Public Function InvokeUsingRequestString(ByVal methodName As String, ByVal sRequest As String,
        Optional ByVal cert As System.Security.Cryptography.X509Certificates.X509Certificate2 = Nothing,
        Optional ByVal ignoreSSLErrors As Boolean = False) As XDocument

        PreInvoke()
        AssertCanInvoke(methodName)

        Dim req As HttpWebRequest = DirectCast(WebRequest.Create(Url), HttpWebRequest)

        If ignoreSSLErrors Then
            req.ServerCertificateValidationCallback() = Function(sender, certificate, chain, sslPolicyErrors) True
        End If

        req.ContentType = "text/xml;charset=""utf-8"""
        req.Accept = "text/xml"
        req.Method = "POST"

        If cert IsNot Nothing Then
            req.ClientCertificates.Add(cert)
        End If

        'If Not String.IsNullOrWhiteSpace(sAuth) Then
        '    req.Headers.Add("Authorization", "Basic " + sAuth)
        'End If

        If Not String.IsNullOrWhiteSpace(Me.Username) AndAlso Not String.IsNullOrWhiteSpace(Me.Password) Then
            req.Credentials = GetCredential()
            req.PreAuthenticate = True
        End If

        Using stm As Stream = req.GetRequestStream()
            Using stmw As New StreamWriter(stm)
                stmw.Write(sRequest)
            End Using
        End Using

        Dim respuesta As String = String.Empty
        Dim resultado As String = String.Empty
        Dim sbError As New Text.StringBuilder
        sbError.AppendLine("<WebExceptionsList>")

        Try
            Using responseReader As New StreamReader(req.GetResponse().GetResponseStream())
                respuesta = responseReader.ReadToEnd()
            End Using
            For Each linea As String In respuesta.Split(New String() {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries)
                If Not (linea.StartsWith("Content-") OrElse linea.StartsWith("--uuid:")) Then
                    resultado += linea & Environment.NewLine
                End If
            Next
        Catch exTO As TimeoutException
            resultado = "<WebException>Tiempo de espera agotado. El servidor del servicio web no respondió a la petición.</WebException>"
        Catch ex As WebException
            Using response As WebResponse = ex.Response
                Dim httpResponse As HttpWebResponse = DirectCast(response, HttpWebResponse)
                If httpResponse IsNot Nothing Then
                    Try
                        sbError.AppendLine(String.Format("<WebException>({0}) {1}</WebException>", DirectCast(httpResponse.StatusCode, Integer), httpResponse.StatusDescription))
                    Catch ex2 As Exception
                        sbError.AppendLine("<WebException>Error desconocido del servidor del servicio web.</WebException>")
                    End Try
                Else
                    sbError.AppendLine("<WebException>No hay objeto de tipo HttpWebResponse.</WebException>")
                End If
                Dim sRespuesta As String = String.Empty

                If response IsNot Nothing Then
                    Try
                        Using data As Stream = response.GetResponseStream()
                            Using reader = New StreamReader(data)
                                sRespuesta = reader.ReadToEnd()
                            End Using
                        End Using
                    Catch ex2 As Exception
                        resultado = sbError.ToString
                    End Try
                Else
                    sbError.AppendLine("<WebException>No hay objeto de tipo WebResponse.</WebException>")
                End If

                If sRespuesta IsNot Nothing AndAlso sRespuesta.Length > 0 Then
                    Dim xRespuesta As New XDocument
                    Try
                        xRespuesta = XDocument.Parse(sRespuesta)
                        resultado = sRespuesta
                    Catch ex3 As Exception
                        'Nada
                    End Try
                    If xRespuesta.ToString.Length < 1 Then
                        resultado = sbError.ToString
                    End If
                Else
                    resultado = sbError.ToString
                End If
                sbError.AppendLine(String.Format("<WebException>{0}</WebException>", ex.ToString))
                sbError.AppendLine("</WebExceptionsList>")
                resultado = sbError.ToString
            End Using
            If String.IsNullOrWhiteSpace(resultado) Then
                sbError.AppendLine(String.Format("<WebException>{0}</WebException>", ex.ToString))
                sbError.AppendLine("</WebExceptionsList>")
                resultado = sbError.ToString
            End If
        End Try

        Dim unescapedString As String = UnescapeString(resultado.Trim)
        Try
            ResponseSOAP = XDocument.Parse(unescapedString.Trim)
        Catch ex As XmlException
            ResponseSOAP = XDocument.Parse(resultado.Trim)
        End Try

        PosInvoke()

        Me.ResultString = ResponseSOAP.ToString
        Me.ResultXML = ResponseSOAP

        Return ResponseSOAP
    End Function
    ''' <summary>
    ''' Realiza una petición a un servicio web usando un nombre de método, una cadena para la petición, y recogiendo la petición como un array de bytes.
    ''' </summary>
    ''' <param name="methodName">Nombre del método.</param>
    ''' <param name="sRequest">Cadena con la petición que se realizará al servicio web.</param>
    ''' <param name="cert"></param>
    ''' <returns>Un array de bytes con el contenido de la respuesta realizada al servicio web.</returns>
    ''' <remarks>Este método solo debería usarse con descargas que quepan en memoria RAM, teniendo en cuenta las posibles restricciones de memoria que el sistema operativo puda tener para procesos individuales.</remarks>
    Public Function InvokeBinaryUsingRequestString(ByVal methodName As String, ByVal sRequest As String,
        Optional ByVal cert As System.Security.Cryptography.X509Certificates.X509Certificate2 = Nothing,
        Optional ByVal ignoreSSLErrors As Boolean = False) As Byte()

        PreInvoke()
        AssertCanInvoke(methodName)

        Dim req As HttpWebRequest = DirectCast(WebRequest.Create(Url), HttpWebRequest)

        If ignoreSSLErrors Then
            req.ServerCertificateValidationCallback() = Function(sender, certificate, chain, sslPolicyErrors) True
        End If

        req.ContentType = "text/xml;charset=""utf-8"""
        req.Accept = "text/xml"
        req.Method = "POST"

        If cert IsNot Nothing Then
            req.ClientCertificates.Add(cert)
        End If

        'If Not String.IsNullOrWhiteSpace(sAuth) Then
        '    req.Headers.Add("Authorization", "Basic " + sAuth)
        'End If

        If Not String.IsNullOrWhiteSpace(Me.Username) AndAlso Not String.IsNullOrWhiteSpace(Me.Password) Then
            req.Credentials = GetCredential()
            req.PreAuthenticate = True
        End If

        Using stm As Stream = req.GetRequestStream()
            Using stmw As New StreamWriter(stm)
                stmw.Write(sRequest)
            End Using
        End Using

        Dim respuesta As String = String.Empty
        Dim resultado As Byte() = New Byte(0) {}
        Dim sError As String = String.Empty
        Dim sb As New Text.StringBuilder

        Dim binaryBuffer As Byte()

        Using binaryReader As New BinaryReader(req.GetResponse().GetResponseStream())
            binaryBuffer = binaryReader.ReadAllBytes
        End Using

        resultado = binaryBuffer

        PosInvoke()

        Return resultado
    End Function
    ''' <summary>
    ''' This method should be called before each Invoke().
    ''' </summary>
    Friend Sub PreInvoke()
        CleanLastInvoke()
        ' feel free to add more instructions to this method
    End Sub
    ''' <summary>
    ''' This method should be called after each (successful or unsuccessful) Invoke().
    ''' </summary>
    Friend Sub PosInvoke()
        ' feel free to add more instructions to this method

    End Sub
    Private Shared Function InlineAssignHelper(Of T)(ByRef target As T, value As T) As T
        target = value
        Return value
    End Function
#End Region
End Class