Serializzazione in XML

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

La serializzazione in XML è uno dei punti di forza di .NET: sono svariati i casi in cui risulta utile, per non dire... indipensabile! Con qualche piccolo accorgimento è possibile effettuare la stessa operazione anche con le classiche ASP: basta un parser DOM, rispettare un paio di semplici convenzioni (che in OOP sarebbe "implementare un'interfaccia") e poche righe di codice per poter serializzare (e de-serializzare) classi JScript in XML.

JScript / XML, andata e ritorno

L'idea che sta alla base dello script è piuttosto semplice: un oggetto, per essere serializzabile, deve esporre "un'interfaccia" che consenta di estrarne tutte le proprietà (in fase di serializzazione), di crearne una nuova istanza e valorizzarne nuovamente le proprietà (in fase di deserializzazione).
In particolare, per JScript è necessario:

  1. Esporre una proprietà che indichi il nome dell'oggetto (OBJECTNAME). Questa proprietà sarà utilizzata come nodo root dell'XML corrispondente all'oggetto serializzato e per creare la nuova istanza della classe durante la deserializzazione.

  2. Esporre, come array di stringhe, una proprietà che costituisca l'elenco delle proprietà che identificano lo stato dell'oggetto (OBJECTPROPERTIES). Qualora una delle proprietà da serializzare non sia un tipo primitivo o di sistema (stringa, array o data) ma un'altra classe, sarà necessario che la stessa sia serializzabile (implementazione ricorsiva dei metodi di serializzazione e di deserializzazione)

  3. Implementare le proprietà da serializzare (vedi punto precedente) come metodi, tramite gli accessor get (lettura) e set (assegnazione); in pratica... viene seguita la convenzione delle classi Java (dove, non esistendo le proprietà, queste sono universalmente identificabili dai metodi caratterizzati dai prefissi "get" e "set")

  4. Predisporre un costruttore vuoto (senza parametri) per l'oggetto; in realtà, poiché in JScript tutti i parametri di una funzione sono opzionali, non è praticamente necessario nessun accorgimento

Volendo formalizzare questi presupposti in UML potremmo definire un'interfaccia (ISerializable), richiesta dalle classi per poter essere serializzabili, come nel class diagram dell'esempio seguente:

Serializzazione - Class diagram esemplificativo

Terminate le premesse, vediamo l'implementazione delle funzioni di serializzazione e deserializzazioni.

Iniziamo la stesura del codice definendo una classe SerializationManager e alcune costanti usate per la creazione del documento XML:

function SerializationManager(){};

SerializationManager.NS = "xmlns:gdt=\"http://www.guru4.net\"";
SerializationManager.TAG_LIST = "gdt:list";
SerializationManager.TAG_ELEMENT = "gdt:item";
SerializationManager.TAG_ELEMENT_ATTRIBUTE_NAME = "name";
SerializationManager.xmlparser = null;

Serializzazione: da oggetto a XML

A questo punto passiamo direttamente alla funzione di serializzazione, object2xml:

SerializationManager.object2xml = function ( obj )
{
    // serialize a null object
    if ( obj == null ) return "";

    // serialize a node value
    if ( typeof ( obj ) != "object" )
        return ( Server.HTMLEncode( obj + "" ) );

    var islist;
    var objprops;
    var objname;
    var xml;
    var ns;
        
    islist = ( typeof ( obj ) == "object" && obj.constructor.toString().indexOf("function Array")!=-1 );    
        
    if ( islist )
    {
        objname = SerializationManager.TAG_LIST;        
        ns = " " + SerializationManager.NS;
    }
    else
    {
        objname = obj.OBJECTNAME;
        objprops = obj.OBJECTPROPERTIES;
        ns = "";
    }
    
    xml = "<" + objname + ns + ">";
    
    if ( islist )
    {    
        // determine if associative array or linear array
        var keycount = 0;
        for ( var k in obj ) keycount++;        
        for (var k in obj)        
            xml += "<" + SerializationManager.TAG_ELEMENT +
                    ((obj.length!=keycount)?(" " +SerializationManager.TAG_ELEMENT_ATTRIBUTE_NAME + "=\"" + Server.HTMLEncode(k) + "\""):"") +
                    ">" + SerializationManager.object2xml ( obj[k] ) +
                    "</" + SerializationManager.TAG_ELEMENT + ">";                
    }
    else
    {
        // serialize an object        
        for (var i=0;i<objprops.length;i++)
        {                
            var ref;            
            eval( "ref = obj.get" + objprops[i] + "()" );            
            xml = xml + "<" + objprops[i] + ">" +
                SerializationManager.object2xml ( ref ) +
                "</" + objprops[i] + ">";    
        }
    }
    
    xml = xml + "</" + objname + ">";
    return xml;    
    
}

Fondamentalmente ci si limita a distinguere se l'elemento per cui è richiesta la serializzazione è un array (nel qual caso viene determinato il tipo: array list oppure dictionary) o un oggetto, quindi si ricorre serializzando e innestando all'XML risultante gli elementi gerarchicamente contenuti.

Deserializzazione: da XML ad oggetto

Dato un XML, per ricostruire l'oggetto corrispondente, abbiamo a disposizione due metodi: xmlfile2object (se il documento XML è contenuto in un file) oppure xml2object (se l'XML è disponibile come stringa):

SerializationManager.xmlfile2object = function( fl )
{
    var xmldoc = Server.CreateObject("Microsoft.XMLDOM");
    xmldoc.async = false;
    xmldoc.load( fl );    
    return SerializationManager.node2object ( xmldoc.documentElement );    
}

SerializationManager.xml2object = function( sxml )
{
    var xmldoc = Server.CreateObject("Microsoft.XMLDOM");
    xmldoc.async = false;
    xmldoc.loadXML(sxml);    
    return SerializationManager.node2object ( xmldoc.documentElement );    
}

Entrambi i metodi richiamano la funzione node2object che si occupa di effettuare fisicamente la deserializzazione:

SerializationManager.node2object = function( node )
{    
    // null node
    if ( node == null ) return null;

    // text node
    if ( node.nodeType == 3 || node.nodeType == 4)
    {
        var retval = node.nodeValue + "";
        return ( retval=="" ) ? null: retval;
    }
        
    // object node
    if ( node.nodeName != SerializationManager.TAG_LIST )
    {
        var obj = eval ("new " + node.nodeName + "()");
        for ( var i=0;i<node.childNodes.length;i++)
        {
            if ( node.childNodes[i].hasChildNodes )
            {
                var p = SerializationManager.node2object (node.childNodes[i].firstChild);
                obj["set" + node.childNodes[i].nodeName](p);                
            }
        }
        return obj;
    }
    
    // list node
    if ( node.nodeName == SerializationManager.TAG_LIST )
    {
        // create node ref
        var l = new Array();
        for ( var i=0;i<node.childNodes.length;i++ )
        {
            var cn = node.childNodes[i];
            if ( cn.nodeName == SerializationManager.TAG_ELEMENT )
            {
                if (cn.attributes.getNamedItem(SerializationManager.TAG_ELEMENT_ATTRIBUTE_NAME)!=null)        
                    l[cn.attributes.getNamedItem(SerializationManager.TAG_ELEMENT_ATTRIBUTE_NAME).nodeValue + ""] =
                    SerializationManager.node2object( cn.firstChild );
                else
                    l[l.length] = SerializationManager.node2object( cn.firstChild );
            }
        }
        return l;
    }    
    return null;
}

Questa funzione è l'esatto opposto di object2xml vista in precedenza: viene processato ricorsivamente il nodo XML (partendo dal document element) e, per ogni nodo, si istanza l'oggetto corrispondente, distinguendo tra array list, array associativi (dictionary), oggetti, tipi primitivi (valori finali in forma di stringa) o valori nulli (null)

Serializzare date

Per migliorare il supporto di tipi primitivi, è possibile forzare la serializzazione automatica delle date semplicemente aggiungendo al prototype dell'oggetto Date di JScript le proprietà OBJECTNAME e OBJECTPROPERTIES. Per ottenere lo stato dell'istanza della data verrà utilizzata la proprietà Time (corrispondente ai millisecondi intercorsi dal 1/1/1970) e già esposta con gli accessor getTime e setTime:

Date.prototype.OBJECTNAME = "Date";
Date.prototype.OBJECTPROPERTIES = new Array("Time");

Esempio di utilizzo

Realizziamo un esempio per dimostrare il funzionamento del processo di serializzazione e deserializzazione (la pagina dimostrativa richiede l'inclusione dello script in allegato).

Definiamo una semplice classe User (serializzabile) e costruiamo una lista di due utenti (l'array "ul1"), lo serializziamo (il risultato della serializzazione viene archiviato nella variabile "uls") e, utilizzando il processo di serializzazione, dichiariamo un secondo array di utenti ("ul2"). Effettuando un loop sui due array confrontiamo i singoli elementi per verificare la consistenza del processo:

<%@Language="JScript"%>
<!--#include file="serializationmanager.asp"-->
<%
function User(username, password, birthdate)
{
    var m_username = (username + "" == "undefined") ? "" : username;
    var m_password = (password + "" == "undefined") ? "" : password;
    
    this.getUsername = function() { return m_username; }
    this.setUsername = function(val) { m_username = val; }
    
    this.getPassword = function() { return m_password; }
    this.setPassword = function(val) { m_password = val; }
    
    this.getEncryptedPassword = function()
    {
        var ep = "";
        for(var i = 0; i < m_password.length; i++)
            ep += "*";
        return ep;
    }
    
    // XMLObject Interface
    this.OBJECTNAME = "User";
    this.OBJECTPROPERTIES = new Array("Username", "Password");
    
}

var ul1 = new Array();
ul1[0] = new User("Mario", "pippo");
ul1[1] = new User("Giovanni", "paperino");

// serialize
var uls = SerializationManager.object2xml(ul1);

// deserialize
var ul2 = SerializationManager.xml2object(uls);

// check result
for(var i = 0; i < ul2.length; i++)
{
    var resultok = ( (ul1[i].getUsername() == ul2[i].getUsername()) && (ul1[i].getPassword() == ul2[i].getPassword()) );
    if(resultok)
        Response.Write("Serializzazione / deserializzazione elemento " + i + ": OK!<br>");
    else
        Response.Write("ERRORE nella serializzazione / deserializzazione dell'elemento " + i + "<br>");    
}
%>

Si noti che, sebbene la classe possa esporre anche altre proprietà o metodi (nell'esempio vi è un metodo / proprietà in sola lettura e calcolata, chiamata "EncryptedPassword") solo quanto strettamente necessario a ricostruire lo stato di ogni singola istanza deve essere indicato per la serializzazione.

L'output visualizzato nel browser eseguendo la pagina dimostrativa è il seguente:

Serializzazione / deserializzazione elemento 0: OK!
Serializzazione / deserializzazione elemento 1: OK!

Come desideravamo, il processo di deserializzazione ha rigenerato oggetti identici a quelli di partenza.

A titolo informativo, il prodotto della serializzazione della lista dell'esempio risulta:

<gdt:list xmlns:gdt="http://www.guru4.net">
    <gdt:item>
        <User>
            <Username>Mario</Username>
            <Password>pippo</Password>
        </User>
    </gdt:item>
    <gdt:item>
        <User>
            <Username>Giovanni</Username>
            <Password>paperino</Password>
        </User>
    </gdt:item>
</gdt:list>