ASP SOAP Client

Se ritieni utile questo articolo, considera la possibilità di effettuare una donazione (il cui importo è a tua completa discrezione) tramite PayPal. Grazie.

Consumare un Web Service utilizzando ASP non è una cosa semplicissima, come, ad esempio, lo è con con .NET. Cercando in rete è possibile trovare diverse implementazioni di client ASP ma, quasi tutte, si limitano ad effettuare un parsing della risposta, trattandola come se fosse un semplice documento XML, con tutti i limiti che questa semplificazione comporta. In realtà, con un po' di pazienza e poche righe di script, è possibile sfruttare la descrizione del servizio (WSDL, Web Service Description Language) per processare in modo trasparente la risposta ad una chiamata di un metodo di un Web Service, utilizzando direttamente i tipi di ritorno (indipendentemente dalla loro complessità) riproducendo così quella che è la generazione della classe proxy in .NET (cioè quello che fa Visual Studio all'aggiunta di un Web Reference o il tool dell'SDK "wsdl.exe").

Interfaccia ed implemetazione del client SOAP

Prima di analizzare il codice del client SOAP, prestate attenzione alle seguenti note:

  • se non avete molta familiarità con i Web Service potete leggere l'articolo Introduzione ai Web Service con .NET, così da poter comprendere pienamente il resto dell'articolo.

  • il client SOAP è realizzato utilizzando come linguaggio di scripting JScript per avere la possibilità di creare a runtime strutture dati complesse, costruite sulla risposta SOAP, come definito dal WSDL del servizio.

Analizziamo l'interfaccia esposta e la relativa implementazione del client SOAP:

Inizializzazione della chiamata

Il client SOAP dovrà essere inizializzato con i parametri del servizio che desideriamo interrogare, ovvero:

  • URL del servizio, impostato tramite il metodo setServiceUrl

  • NameSpace del servizio, impostato tramite il metodo setServiceNamespace

  • Nome del metodo da richiamare, impostato tramite il metodo setMethodName

  • Impostazione dei parametri di interrogazione specifici per il metodo da richiamare, definiti richiamando il metodo addParameter che accetta due argomenti, il nome del parametro da impostare ed il corrispondente valore

Definiamo la nostra classe ed i metodi di inizializzazione del servizio:

function SOAPClient()
{
    var m_serviceurl;
    var m_servicenamespace = "";
    var m_methodname;    
    var m_parameters = new Array();

    this.setServiceUrl = function(val)
    {
        m_serviceurl = val;
    }

    this.setServiceNamespace = function(val)
    {
        m_servicenamespace = val;
    }
    
    this.setMethodName = function(val)
    {
        m_methodname = val;
    }

    this.addParameter = function(pname, pval)
    {
        m_parameters[pname] = pval;
    }
}

Effettuare la chiamata al Web Service

Dopo aver definito tutte le caratteristiche del Web Service che desideriamo interrogare, passiamo ad analizzare il metodo principale, call():

this.call = function()
{
    // load WSDL
    m_wsdl = Server.CreateObject("Microsoft.XMLDOM") ;
    m_wsdl.load(m_serviceurl + "?wsdl");            
        
    // build SOAP request
    var sXml =
        "<?xml version=\"1.0\" ?>" +
        "<soap:Envelope " +
        "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
        "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
        "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
        "<soap:Body>" +
        "<" + m_methodname + " xmlns=\"" + m_servicenamespace + "\">";
    for(var i in m_parameters)
        sXml += "<" + i + ">" + m_parameters[i] + "</" + i + ">";
    sXml += "</" + m_methodname + "></soap:Body></soap:Envelope>";
    var xmlHTTP = Server.CreateObject("Msxml2.XMLHTTP");
    xmlHTTP.Open("Post", m_serviceurl, false);
    xmlHTTP.setRequestHeader("SOAPAction", m_servicenamespace + "/" + m_methodname);
    xmlHTTP.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlHTTP.Send(sXml);
    
    // set raw xml
    m_rawxml = xmlHTTP.responseXML.xml + "";
    
    // .NET way - the only way :-)
    var nd = xmlHTTP.responseXML.selectSingleNode("//" + m_methodname + "Result");
    if(nd == null)
    {
        if(xmlHTTP.responseXML.selectSingleNode("//faultcode/text()"))
            throw new Error(500, xmlHTTP.responseXML.selectSingleNode("//faultcode/text()").nodeValue + " - " + xmlHTTP.responseXML.selectSingleNode("//faultstring/text()").nodeValue);
        else
            return null;
    }
    return this.soapresult2object(nd);
}

Analizzando il codice sopra possiamo notare che:

  1. viene caricata in un parser XML (field privato m_wsdl) la descrizione del servizio, richiamando l'URL del Web Service con l'aggiunta del parametro ?wsdl. Il documento XML di descrizione verrà utilizzato in seguito per costruire i tipi di ritorno corretti

  2. utilizzando i parametri di inizializzazione definiti in precedenza, viene costruita la richiesta SOAP (sXml) da inviare al Web Service

  3. attraverso XMLHTTP la richiesta SOAP viene inviata al Web Service in modo sincrono; la risposta viene quindi salvata nella variabile m_rawxml, che è anche possibile esporre come proprietà del client SOAP per essere utilizzata direttamente al di fuori del SOAPClient

  4. la risposta del Web Service viene processata per costruire il tipo di ritorno (compreso un eventuale errore ricevuto dal server). Si noti che, pur essendo SOAP uno standard, in realtà ogni produttore ha implementato in modo sensibilmente diverso la costruzione della risposta. Il codice presentato è in grado di processare le risposte ottenute da un servizio .NET. Qualora si voglia interrogare servizi esposti con tecnologia differente (Java, PHP, ecc.) sarà necessario valutare la struttura XML della risposta in modo specifico.

Costruzione del risultato della chiamata

Per la costruzione del tipo di ritorno viene chiamato in modo ricorsivo il metodo node2object:

this.soapresult2object = function(node)
{
    return this.node2object(node);
}

this.node2object = function(node)
{    
    // null node
    if(node == null)
        return null;
    // text node
    if(node.nodeType == 3 || node.nodeType == 4)
        return this.extractValue(node);

    // leaf node
    if (node.hasChildNodes() && node.childNodes.length==1 && (node.firstChild.nodeType == 3 || node.firstChild.nodeType == 4))
        return this.node2object(node.firstChild);
    var isarray = false;
    var el = m_wsdl.selectSingleNode("//s:element[@name='" + node.nodeName + "']");
    isarray = (el!=null && el.attributes.getNamedItem("type")!=null && (el.attributes.getNamedItem("type").nodeValue + "").toLowerCase().indexOf("arrayof") != -1);

    // object node
    if(!isarray)
    {
        var obj = null;
        if(node.hasChildNodes())
            obj = new Object();
        for(var i = 0; i < node.childNodes.length; i++)
        {
            var p = this.node2object(node.childNodes[i]);
            obj[node.childNodes[i].nodeName] = p;
        }
        return obj;
    }

    // list node
    else
    {
        // create node ref
        var l = new Array();
        for(var i = 0; i < node.childNodes.length; i++)
        {
            var cn = node.childNodes[i];
            l[l.length] = this.node2object(cn);
        }
        return l;
    }
    return null;
}

che internamente richiama extractValue per effettuare il casting dei tipi primitivi sulla base del WSDL:

this.extractValue = function(node)
{
    var value = node.nodeValue;
    var el = m_wsdl.selectSingleNode("//s:element[@name='" + node.parentNode.nodeName + "']");
    var type = (el != null && el.attributes.getNamedItem("type") != null) ? (el.attributes.getNamedItem("type").nodeValue + "").toLowerCase() : null;
    switch(type)
    {
        default:
        case "s:string":            
        {
            return (value != null) ? value + "" : "";
        }
        case "s:boolean":
        {
            return value+"" == "true";
        }
        case "s:int":
        case "s:long":
        {
            return (value != null) ? parseInt(value + "", 10) : 0;
        }
        case "s:double":
        {
            return (value != null) ? parseFloat(value + "") : 0;
        }
        case "s:datetime":
        {
            if(value == null)
                return null;
            else
            {
                value = value + "";
                value = value.substring(0, value.lastIndexOf("."));
                value = value.replace(/T/gi," ");
                value = value.replace(/-/gi,"/");
                var d = new Date();
                d.setTime(Date.parse(value));                                        
                return d;                
            }
        }
    }        
}

Esempio di utilizzo

Si supponga di avere un Web Service per l'accesso ad una customer base esterna che esponga un metodo con la seguente segnatura:
login(string username, string password) : User
Il metodo per l'autenticazione restituirà quindi un oggetto di tipo "User" (ipoteticamente caratterizzato da ID, Firstname, LastName, e SubscriptionDate) se i dati di autenticazioni sono validi oppure null qualora la login abbia esito negativo.
L'esempio di codice ASP seguente mostra l'interrogazione del Web Service (URL e namespace sono chiaramente fittizzi):

<% @Page Language="JScript" %>
<!--#include file="soapclient.asp"-->
<%
// recupero i dati di autenticazione dal form di login:
var username = Request.Form["username"];
var password = Request.Form["password"]

// preparazione della chiamata al Web Service:
var sc = new SOAPClient();
sc.setServiceUrl("http://www.altrosito.com/services/customer.asmx");
sc.setServiceNamespace("http://www.altrosito.com/services");
sc.setMethodName("Login");
sc.addParameter("username", username);
sc.addParameter("password", password);

// chiamata e verifica login:
var u = sc.call();
if(u != null)    // login OK: visualizzazione del profilo utente
{
    Response.Write("Benvenuto " + u.Firstname + " " + u.Lastname + "!<br />");
    Response.Write("Il tuo identificativo è " + u.ID.ToString() + "<br />");
    Response.Write("Sei iscritto dall'anno " + u.SubscriptionDate.getFullYear());
}
else    // login non valida
{
    Response.Write("Dati non validi. Riprova.")
}
%>

Semplice, no?

Utilizzo client-side

Il client SOAP è, salvo alcune modifiche, utilizzabile anche client-side. Il porting ha complessità differente in funzione di quanti e quali browser desideriamo supportare. Per quanto riguarda Internet Explorer è sufficiente sostuire i metodi di creazione dei componenti COM da "Server.CreateObject" a "new ActiveXObject". In generale possiamo pensare di utilizzare librerie Open Source e cross-borwser per ottenere i componenti XMLHTTP e XMLDOMDocument; ne esistono diverse, più o meno o complesse e potenti: basta una breve ricerca in Internet per individuare quella che meglio si adatta alle nostre esigenze.
Un limite che dobbiamo tenere però presente nelle chiamate effettuate via JavaScript all'interno di un client è quello delle limitazioni imposte per motivi di sicurezza: molti browser non consentono di effettuare chiamate remote esterne al dominio della pagina corrente.

Nell'articolo JavaScript SOAP Client viene fornita un'implementazione completa della libreria in versione client-side, compatibile con i principali browser e che consente di effettuare chiamate sia in modalità sincrona che asincrona (AJAX).