MokaByte 80 - Dicembre 2003
Jakarta Commons
III parte: Jakarta Betwixt
di
Alessandro "Kazuma" Garbagnati
Sebbene ancora in versione alpha, il componente Betwixt del progetto Jakarta Commons risquote già molto interesse dalla comunità degli sviluppatori: è la soluzione che permette la realizzazione di mapping bidirezionali tra documenti XML e JavaBeans. Ecco la soluzione per quando la persistenza dei dati diventa necessaria, ma l'utilizzo di un database non è possibile o non è consigliato.

Introduzione
Quando all'interno di una applicazione è necessario gestire la persistenza dei dati, la miglior soluzione possibile consiste nel ricorrere ad un database e, grazie a JDBC, Java ne facilita l'utilizzo, per non parlare di librerie esterne che ne rendono la gestione ancora più semplice.
Vi sono, però, situazioni nelle quali non si vuole o non si può utilizzare un database, pur necessitando di gestire la persistenza di alcuni dati, normalmente attraverso l'utilizzo di soluzioni che si basano sul filesystem. Sebbene la più diffusa ed utilizzata si basa sulla serializzazione di oggetti, attraverso l'interfaccia java.io.Serializable, col trascorrere del tempo sta sempre più affermandosi la soluzione XML, il cui grosso vantaggio consiste anche nel permettere di intervenire manualmente sui dati.

 

Mapping bidirezionale
Nel precedente articolo di questa serie abbiamo visto come il componente Digester potesse essere utilizzato per effettuare un mapping tra un documento XML ed un JavaBean e degli esempi a supporto dimostrava come questa operazione potesse essere fatta in maniera piuttosto semplice e veloce. Ma, nonostante questo, la soluzione Digester presenta il problema della monodirezionalità, ossia un documento XML può essere "mappato" ad un JavaBean ma non viceversa.
Per ottenere una soluzione bidirezionale è necessario utilizzare un altro componente del progetto Jakarta Commons, Betwixt, che, come recita la descrizione sulla sua homepage, ha lo scopo di trasformare un JavaBean in un documento XML, utilizzando un meccanismo di introspezione simile a quello proposto nelle specifiche di Sun sui JavaBean. In più, per poter completare la bidirezionalità, Betwixt prepara anche le regole di Digester per poter poi effettuare la lettura del documento XML all'interno del bean.


Figura 1 - La homepage del componente Betwixt

Betwixt è un componente ancora giovane e, ad oggi, l'unica versione disponibile per il download pubblico è la Alpha 1. Per poter utilizzare questo componente è necessario, prima di tutto, scaricare il pacchetto contenente i file binari oppure i sorgenti, nel caso in cui vi vogliate dilettare nella compilazione. In entrambi i casi è anche necessario ottenere le librerie di supporto indispensabili per il componente che, attualmente, non sono inserite all'interno dei pacchetti e che sono: Digester (disponibile attraverso la sua homepage http://jakarta.apache.org/commons/digester/index.html), BeanUtils (http://jakarta.apache.org/commons/beanutils.html), Collections (http://jakarta.apache.org/commons/collections.html) e Logging (http://jakarta.apache.org/commons/logging.html). Tutti i vari jar di questi componenti andranno poi inseriti nel classpath, insieme a quello di Betwixt.

 

Da JavaBean a XML
Il primo esempio è piuttosto elementare. Si parte da un JavaBean molto semplice tipo questo:

import java.io.*;
import java.beans.*;
import org.xml.sax.*;
import org.apache.commons.betwixt.io.*;

public class SimpleBean {

/*
Attributi semplici del bean
*/
private long codice;
private String descrizione;
private int valore;

/*
Costruttori
*/
public SimpleBean() {
// nessuna inizializzazione
}
public SimpleBean(long codice, String descrizione, int valore) {
this();
setCodice(codice);
setDescrizione(descrizione);
setValore(valore);
}

/*
Getters & Setters dei vari attributi
*/
public void setCodice(long arg) {
codice = arg;
}
public long getCodice() {
return codice;
}
public void setDescrizione(String arg) {
descrizione = arg;
}
public String getDescrizione() {
return descrizione;
}
public void setValore(int arg) {
valore = arg;
}
public int getValore() {
return valore;
}

/*
Implementazione del metodo toString
*/
public String toString() {
StringBuffer retBuff = new StringBuffer("[");
retBuff.append("Codice=").append(getCodice()).append(",");
retBuff.append("Descrizione=").append(getDescrizione()).append(",");
retBuff.append("Valore=").append(getValore());
return retBuff.append("]").toString();
}

}

Sono molte le soluzioni per poter scrivere un metodo di trasformazione in XML del nostro Bean. In questo caso utilizzeremo un metodo simile a toString(), che chiameremo toXMLString(), che ci ritornerà un oggetto di tipo StringBuffer contenente la versione XML dell'oggetto.
Ecco come ottenere questo risultato attraverso Betwixt:

public StringBuffer toXMLString() {

// creazione string writer di output
StringWriter outWriter = new StringWriter();

// crea e configura il bean writer
BeanWriter beanWriter = new BeanWriter(outWriter);
beanWriter.getXMLIntrospector().setAttributesForPrimitives(false);
beanWriter.setWriteIDs(false);
beanWriter.enablePrettyPrint();

Per inizializzare la classe che si occuperà di scrivere l'XML, è necessario avere a disposizione un OutputStream o un Writer. In questo caso, volendo fornire uno StringBuffer, utilizzeremo uno StringWriter.
Una volta creato il nostro scrittore, potremo definire le proprietà in base a come vogliamo ottenere l'output del nostro documento. Nel nostro esempio abbiamo scelto di scrivere tutti i membri del bean come tag e non come attributi, non vogliamo far scrivere l'ID della classe e soprattutto, vogliamo che il documento finale abbia un aspetto leggibile (indentato).


Figura 2 - Betwixt "Getting Started"

Definite le varie proprietà, possiamo quindi passare alla generazione del documento, attraverso il metodo write() al quale forniremo il nome dell'elemento root. Questo metodo genera delle eccezioni che, in questo caso, non vengono realmente gestite. Il consiglio, ovviamente, è di non ignorare mai le eccezioni e di gestirle sempre nel modo più consono.

// scrivi il bean sotto l'elemento root
try {
beanWriter.write("simpleBean", this);
} catch (IOException ioE) {
ioE.printStackTrace();
} catch (SAXException saxE) {
saxE.printStackTrace();
} catch (IntrospectionException iE) {
iE.printStackTrace();
}

// return the String
return outWriter.getBuffer();
}

Una volta completata la scrittura, il nostro metodo ritornerà la versione StringBuffer dello StringWriter creato.

Per completare l'esempio possiamo scrivere un metodo main che potremo utilizzare per testare il nostro oggetto e, soprattutto, per vedere il risultato della nostro metodo di generazione XML.

public static final void main(String[] args) {

// Creazione di un semplice bean e stampa del suo contenuto
SimpleBean bean = new SimpleBean(0L, "ZERO", 1);
System.out.println(bean.toString());

// Scrittura del documento
FileWriter writer = null;
try {

// ottieni la versione XML del bean ed aggiungi la
// linea di definizione
StringBuffer xmlBuffer = bean.toXMLString();
xmlBuffer.insert(0, "<?xml version='1.0' ?>");

// crea e scrivi il file di output
writer = new FileWriter("test.xml");
writer.write(xmlBuffer.toString());

}
catch (IOException ioE) {
ioE.printStackTrace();
}
finally {
try {
writer.close();
}
catch (Throwable t) {
/* ignora */
}
}
}

Se non vi saranno errori inaspettati, questo codice dovrebbe creare un documento chiamato test.xml, contenente la versione XML del bean:

<?xml version="1.0"?>
<simpleBean>
<codice>0</codice>
<descrizione>ZERO</descrizione>
<valore>1</valore>
</simpleBean>

E' possibile cambiare il risultato semplicemente modificando la definizione delle proprietà dello scrittore. Ad esempio facendo scrivere i membri del bean come attributi e non come tag:

beanWriter.getXMLIntrospector().setAttributesForPrimitives(true);

In questo caso il file test.xml generato dal nostro esempio, sarà strutturato in questo modo:

<?xml version="1.0"?>
<simpleBean codice="0" descrizione="ZERO" valore="1" />


Da XML a JavaBean
L'operazione opposta, ossia la creazione del JavaBean partendo dal documento XML viene garantita da Betwixt grazie all'uso di Digester. Betwixt, infatti, ne genera tutte le regole necessarie in modo da poter leggere il documento XML ed ottenere l'oggetto richiesto.
Per provare questa operazione, possiamo utilizzare un approccio molto elementare, scrivendo un semplice programmino contenente il solo metodo main.

import java.io.*;
import java.beans.*;
import org.apache.commons.betwixt.io.*;
import org.xml.sax.*;

public class SimpleBeanReadAndWrite {

public static final void main(String[] args) {

Per semplificare tutto possiamo utilizzare il JavaBean costruito in precedenza. Una volta inizializzato con dei valori qualsiasi, possiamo ottenerne la versione XML attraverso il metodo toXMLString() e la creazione di uno StringReader.

// crea il bean
SimpleBean bean = new SimpleBean(1L, "ALEX", 100);

// ottieni la sua versione XML
StringBuffer xmlBean = bean.toXMLString();
xmlBean.insert(0, "<?xml version='1.0' ?>");

// crea un reader
StringReader xmlReader = new StringReader(xmlBean.toString());

Ottenuto un reader contenente l'oggetto in formato XML, è necessario istanziare un BeanReader, tramite il quale Betwixt potrà generare il JavaBean. Così come per lo scrittore, anche il lettore ha la necessità di essere configurato attraverso la definizione di alcuni parametri. E' ovvio che i parametri devono essere impostati nello stesso modo, altrimenti la lettura non avverrebbe in modo corretto.
I questo esempio, quindi, possiamo definire che gli attributi del bean sono tag e che non intendiamo utilizzare l'ID della classe.

// crea e configura il Bean Reader
BeanReader beanReader = new BeanReader();
beanReader.getXMLIntrospector().setAttributesForPrimitives(false);
beanReader.setMatchIDs(false);

Prima di poter leggere il JavaBean è necessario registrare il tipo di oggetto che il nostro BeanReader dovrà leggere, fornendogli il nome del tag principale del nostro documento XML e la classe alla quale questo bean appartiene.
Fatto questo potremo effettuare la lettura, attraverso il metodo parse(), che l'oggetto BeanReader eredita proprio dall'oggetto Digester. Entrambi i metodi (la registrazione ed il parsing) generano delle eccezioni ed è quindi importante ricordare che in questo esempio queste non sono gestite in modo serio, ma durante lo sviluppo di applicazioni serie, le gestione delle eccezioni è fondamentale.

// Registra e leggi il bean
try {
beanReader.registerBeanClass("simpleBean", SimpleBean.class);
bean = (SimpleBean)beanReader.parse(xmlReader);

} catch (IOException ioE) {
ioE.printStackTrace();
} catch (SAXException saxE) {
saxE.printStackTrace();
} catch (IntrospectionException iE) {
iE.printStackTrace();
}

// se il bean non e' nullo, "stampalo" a video
if (bean != null) {
System.out.println(bean.toString());
}

}
}

Per concludere il programma e verificare che la lettura sia andata a buon fine, facciamo scrivere a video il bean appena letto, utilizzando il suo toString().

Nel mondo reale è comunque piuttosto difficile che il documento XML da cui generare un JavaBean sia generato all'interno dello stesso metodo, come abbiamo visto in questo esempio. Probabilmente il documento di partenza è presente su file, oppure proviene da una fonte esterna come un database oppure la rete internet.
Se vogliamo utilizzare il documento XML che abbiamo generato in precedenza, possiamo modificare il metodo main in qualcosa di simile:

public static final void main(String[] args) {

SimpleBean bean = null;

// Registra e leggi il bean
try {
beanReader.registerBeanClass("simpleBean", SimpleBean.class);
bean = (SimpleBean)beanReader.parse(new FileReader("test.xml"));

} catch (IOException ioE) {
ioE.printStackTrace();
} catch (SAXException saxE) {
saxE.printStackTrace();
} catch (IntrospectionException iE) {
iE.printStackTrace();
}

// se il bean non e' nullo, "stampalo" a video
if (bean != null) {
System.out.println(bean.toString());
}

}


Non solo primitive
Il bean che è stato utilizzato negli esempi precedenti è composto solo da primitive o, comunque, oggetti molto semplici (in questo caso String). Betwixt è, ovviamente, in grado di gestire allo stesso modo anche i JavaBean i cui attributi possono essere più complessi, come altri oggetti, array o anche collezioni.
La logica è ovviamente la stessa e l'unica cosa che cambia, nel caso in cui si usino collezioni o arrays, è la presenza di un metodo il cui scopo è quello di aggiungere un nuovo elemento all'array o alla collezione, che verrà utilizzato dalla parte di lettura di Betwixt. Questo metodo dovrà chiamarsi "add", seguito dal nome dell'attributo.

Anche in questo caso, ecco un esempio per vedere come Betwix si comporta con JavaBean più complessi:

import java.io.*;
import java.util.*;
import java.beans.*;
import org.apache.commons.betwixt.io.*;
import org.xml.sax.*;

public class ComposedBean {

/*
Attributi del bean
*/
int codice;
SimpleBean bean;
List datiList;
int[] datiArray;


/*
Costruttori
*/
public ComposedBean() {
datiList = new ArrayList();
datiArray = new int[0];
}
public ComposedBean(int codice, SimpleBean bean) {
this();
setCodice(codice);
setBean(bean);
}


/*
Getters & Setters dei vari attributi
*/
public void setCodice(int arg) {
codice = arg;
}
public int getCodice() {
return codice;
}

public void setBean(SimpleBean arg) {
bean = arg;
}
public SimpleBean getBean() {
return bean;
}

public void setDatiList(List arg) {
datiList = arg;
}
public List getDatiList() {
return datiList;
}
public void addDatiList(int arg) {
datiList.add(new Integer(arg));
}

public void setDatiArray(int[] arg) {
datiArray = arg;
}
public int[] getDatiArray() {
return datiArray;
}
public void addDatiArray(int arg) {
int[] newArray = new int[datiArray.length + 1];
for (int i=0; i<datiArray.length; i++) {
newArray[i] = datiArray[i];
}
newArray[datiArray.length] = arg;
datiArray = newArray;
}


/*
Implementazione metodo toString()
*/
public String toString() {
StringBuffer retBuff = new StringBuffer("[");
retBuff.append("Codice=").append(getCodice()).append(",");
retBuff.append("Bean=").append(getBean()).append(",");
retBuff.append("DatiList=");
for (int i=0; i<datiList.size(); i++) {
retBuff.append(datiList.get(i));
if (i < (datiList.size()-1)) {
retBuff.append(",");
}
}
retBuff.append(",");
retBuff.append("DatiArray=");
for (int i=0; i<datiArray.length; i++) {
retBuff.append(datiArray[i]);
if (i < (datiArray.length-1)) {
retBuff.append(",");
}
}
return retBuff.append("]").toString();
}


/*
Implementazione metodo toXMLString()
*/
public StringBuffer toXMLString() {

// creazione string writer di output
StringWriter outWriter = new StringWriter();

// crea e configura il bean writer
BeanWriter beanWriter = new BeanWriter(outWriter);
beanWriter.getXMLIntrospector().setAttributesForPrimitives(false);
beanWriter.setWriteIDs(false);
beanWriter.enablePrettyPrint();

// scrivi il bean sotto l'elemento root
try {
beanWriter.write("composedBean", this);
} catch (IOException ioE) {
ioE.printStackTrace();
} catch (SAXException saxE) {
saxE.printStackTrace();
} catch (IntrospectionException iE) {
iE.printStackTrace();
}

// return the String
return outWriter.getBuffer();
}


/*
Metodo main di test
*/
public static final void main(String[] args) {

// creazione di un SimpleBean e di un ComposedBean
// che lo contenga
SimpleBean simpleBean = new SimpleBean(1L, "SIMPLE", 1);
ComposedBean bean = new ComposedBean(1, simpleBean);

// inserimento alcuni valori nella lista e nell'array
bean.addDatiList(0);
bean.addDatiList(1);
bean.addDatiList(2);
bean.addDatiArray(10);
bean.addDatiArray(11);
bean.addDatiArray(12);

// Generazione file XML su disco
FileWriter writer = null;
try {
writer = new FileWriter("./composed.xml");
writer.write(xmlBuffer.toString());
}
catch (IOException ioE) {
ioE.printStackTrace();
}
finally {
try { writer.close(); } catch (Exception e) { /* ok */ }
}
}
}

E' possibile notare come il codice di questa classe sia sensibilmente simile a quello della sua versione semplice. Infatti, a parte i due metodi addDatiArray() e addDatiList(), non vi sono differenze sostanziali, nemmeno sul metodo toXMLString().

Nemmeno la lettura del documento XML subisce modifiche. L'unica modifica che potremmo apportare nel codice main del programma che abbiamo visto in precedenza per la lettura del bean semplice, riguarderebbe solo poche linee:

public static final void main(String[] args) {

ComposedBean bean = null;

// Registra e leggi il bean
try {
beanReader.registerBeanClass("composedBean", ComposedBean.class);
bean = (SimpleBean)beanReader.parse(new FileReader("./composed.xml"));

} catch (IOException ioE) {
ioE.printStackTrace();
} catch (SAXException saxE) {
saxE.printStackTrace();
} catch (IntrospectionException iE) {
iE.printStackTrace();
}

// se il bean non e' nullo, "stampalo" a video
if (bean != null) {
System.out.println(bean.toString());
}

}


Conclusioni
Betwixt non è l'unica soluzione per il mapping tra XML e JavaBean e probabilmente in rete è possibile trovare altre librerie sia open source che commerciali, anche in stati di sviluppo più avanzati, se non già ufficialmente rilasciate, contrariamente a Betwixt che, come già detto in precedenza, è un componente ancora giovane.


Figura 3
- La "ToDo page" del progetto. C'è ancora un po' di strada da fare...

Il suo sviluppo, infatti, procede piuttosto lentamente, considerato che, ad oggi, l'unica versione pubblica disponibile sul sito (http://jakarta.apache.org/commons/betwixt/index.html) è la Alpha 1, di fine gennaio 2003. A difesa, però, bisogna sottolineare che in fondo i test e le prove effettuate sulla librerie hanno dato esiti positivi e le stesse prestazioni si sono rivelate adeguate.
Nonostante questo, credo che il componente sia ancora troppo giovane per essere utilizzato in ambienti di produzione, almeno sino a quando il gruppo di lavoro che si occupa del suo sviluppo ne rilasci una versione più stabile. Ma per prototyping o per lo sviluppo di piccole applicazioni non critiche Betwixt può essere tranquillamente preso in considerazione.

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it