MokaByte 79 - 9mbre 2003 
Jakarta Commons
II parte: Jakarta Digester
di
Alessandro "Kazuma" Garbagnati
Forse con un po' di presunzione, alcuni hanno voluto definire XML una vera e propria panacea per i programmatori e, a vedere la sua sempre maggiore diffusione, non sembra che questa affermazione sia del tutto errata o fuori luogo.
Grazie alla sua versatilità, infatti, negli ultimi tempi abbiamo visto come per risolvere numerosi problemi di programmazione, gli sviluppatori abbiano adottato soluzioni che utilizzavano XML, qualche volta come collante tra sistemi non omogenei, altre volte come database di supporto ed altre ancora per aiutare a configurare le applicazioni.

SAX & DOM
Indipendentemente dalla motivazione e dall'obiettivo finale, per poter utilizzare un documento XML prima di tutto è necessario potervi accedere e acquisire le informazioni in esso contenute. Questa operazione, comunemente chiamata "parsing", può essere realizzata utilizzando due metodologie differenti, DOM e SAX.
Nel primo caso (Document Object Model) il documento XML viene letto integralmente e, terminata la lettura, viene generata una struttura ad albero che fornisce agli sviluppatori un sistema piuttosto semplice per accedere alle informazioni ed ai dati contenuti all'interno del documento. Nel secondo metodo (Simple Api for XML) che è comunque alla base dello stesso DOM, il documento viene letto e ogni qualvolta viene incontrato un tag, viene generato un evento. La maggior complessità di questa metodologia deriva dal fatto che il compito dello sviluppatore è di gestire questi eventi, a seconda delle proprie necessità e del tipo di applicazione.
Le soluzioni che si basano su DOM sono normalmente le più comuni e diffuse a causa della maggior semplicità ed immediatezza nell'uso ma, allo stesso tempo, nascondono molte insidie, prima fra tutte la maggior richiesta di risorse di sistema che aumentano con l'aumentare della dimensione dei documenti XML da leggere. Con SAX invece il problema si ribalta, visto le ottime prestazioni e la totale assenza di limitazioni. Purtroppo la complessità nella gestione degli eventi spesso scoraggiano gli sviluppatori ad adottare soluzioni che utilizzano questo metodo.



Figura 1
- La homepage di Digester

Una possibile risposta a questo è offerta da "Digester", uno dei componenti del progetto "Jakarta Commons", che propone una metodologia di parsing basata sul metodo SAX, mantenendone potenza e versatilità, ma cercando di renderla più semplice da utilizzare.

 

I concetti di base
La logica di Jakarta Digester è piuttosto semplice e si basa sul concetto di applicare delle regole quando si incontra una determinata sequenza di tag. Per meglio comprendere cosa si intende, partiamo da un semplice documento XML che potrebbe rappresentare un piccolo file di configurazione:

<?xml version="1.0"?>
<configurazione>
<proprieta>
<nome> database.driver </nome>
<valore> com.mysql.jdbc.Driver </valore>
</proprieta>
<proprieta>
<nome> database.url </nome>
<valore> jdbc:mysql://localhost/database </valore>
</proprieta>
<proprieta>
<nome> username </nome>
<valore> dba </valore>
</proprieta>
<proprieta>
<nome> password </nome>
<valore> dba123 </valore>
</proprieta>
</configurazione>

Digester identifica i nodi della struttura originale del documento secondo una logica molto simile a quella di XPath, paragonabile a quella che viene utilizzata comunemente per i file system. Ogni nodo viene quindi visto in questo modo:

configurazione
configurazione/proprieta
configurazione/proprieta/nome
configurazione/proprieta/valore

Quando Digester, durante la lettura del documento, incontra una di queste sequenze, esegue la regola ad essa associata, se esiste. Nel nostro caso, ad esempio, potremmo voler inserire ogni proprietà, caratterizzata dalla combinazione del contenuto dei tag "nome" e "valore", all'interno di un oggetto di configurazione. Per fare questo dobbiamo associare alla sequenza "configurazione/proprieta" l'esecuzione di un metodo che utilizza i valori contenuti nelle due sequenze "configurazione/proprieta/nome" e "configurazione/proprieta/valore". Vi assicuro, è più difficile da spiegare che da realizzare.

Ma per poter passare dalla teoria ai fatti bisogna prima acquisire tutti i "pezzi" necessari, partendo dalla homepage del componente Digester, all'indirizzo http://jakarta.apache.org/commons/digester.html. All'interno di questa pagina è presente il link per poter scaricare l'ultima versione del pacchetto (sorgenti o versione compilata).
E' molto probabile che questo pacchetto contenga solo il jar di Digester e non le altre librerie che sono richieste per il suo funzionamento. Se così fosse, nessun panico. Basta scaricare gli altri componenti, tutti provenienti dallo stesso progetto Jakarta Commons, ossia: 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 Digester.

 

Primo esempio
Una volta preparato l'ambiente, possiamo costruire la classe Configurazione che utilizzeremo per il nostro esempio, limitandoci, all'inizio, alle funzionalità di base:

import java.util.*;

public class Configurazione {

  Properties proprieta;

  
  // Il costruttore si occupa di inizializzare l'oggetto che conterrà le proprietà
  
  public Configurazione() {
    proprieta = new Properties();
  }


  
  // Questo metodo aggiungerà la combinazione nome/valore come proprietà
  public void addProprieta(String nome, String valore) {
    proprieta.put(nome, valore);
  }


  // Questo metodo verrà utilizzato per testare il nostro oggetto
  public String toString() {
    StringBuffer retBuff = new StringBuffer();
    Enumeration enum = proprieta.propertyNames();
    while (enum.hasMoreElements()) {
      String nome = (String)enum.nextElement();
      retBuff.append(nome).append(" = ");
      retBuff.append(proprieta.get(nome));
      if (enum.hasMoreElements()) {
        retBuff.append(System.getProperty("line.separator"));
      }
    }
    return retBuff.toString();
  }

}

Come si può facilmente vedere, non c'è nulla di speciale all'interno di questa classe. L'unico elemento che ha una particolare importanza per l'esempio è il metodo "addProprieta" che è quello che verrà utilizzato da Digester.

Una volta verificato che tutto sia a posto, possiamo iniziare a scrivere il metodo che si occuperà di leggere il documento XML attraverso Digester, che chiameremo semplicemente "parse":

public void parse(String filename) throws IOException, SAXException {

  // Creazione dell'oggetto Digester ed inserimento
  // dell'oggetto Configurazione all'interno dello stack
  Digester digester = new Digester();
  digester.push(this);

La prima operazione consiste nell'inizializzare l'oggetto Digester e nel comunicargli attraverso lo stack su quale oggetto noi intenderemo eseguire le operazioni che seguiranno. Essendo questo oggetto proprio la classe che ospita il metodo, abbiamo utilizzato "this".
A questo punto possiamo definire le regole:

  // Definizione delle regole
  digester.addCallMethod("configurazione/proprieta", "addProprieta", 2);
  digester.addCallParam("configurazione/proprieta/nome", 0);
  digester.addCallParam("configurazione/proprieta/valore", 1);

La prima regola viene eseguita ogni qualvolta viene letto il tag di partenza di una proprietà. In questo caso indichiamo che deve essere chiamato il metodo chiamato "addProprieta", passandogli due argomenti.
La seconda e la terza regola sono identiche e servono per definire i due argomenti richiesti dal metodo indicato in precedenza.
A questo punto possiamo semplicemente chiamare il metodo che effettuerà il parsing del documento, applicando le regole da noi definite, completando il nostro metodo.

  // Parsing del documento XML
  digester.parse(filename);
}

Il metodo parse può generare due tipi di eccezioni, la IOException e la SAXException. In questo caso non sono gestite internamente, ma rimandate al metodo chiamante.
Per poter compilare questo oggetto è necessario aggiungere alla lista degli import i due package java.io.* e org.xml.sax.*, necessari per le eccezioni ed il package org.apache.commons.digester.* per tutto quello che riguarda Digester.

Per completare l'esempio e, quindi, provarlo, possiamo aggiungere alla nostra classe il metodo "main". In fase di esecuzione utilizzeremo il primo argomento per passargli il file XML che abbiamo creato in precedenza.

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

    // Inizializza l'oggetto ed esegui il parse
    Configurazione config = new Configurazione();
    config.parse(args[0]);

    // Per verificare che tutto sia ok, eseguiamo il metodo toString()
    System.out.println();
    System.out.println(config.toString());
  
}
  catch (Exception e) {
    e.printStackTrace();
  }
}

Per l'esecuzione è quindi sufficiente fare così (ricordatevi di aver incluso nel classpath tutto il necessario).

java Configurazione "configurazione.xml"

database.driver = com.mysql.jdbc.Driver
password = dba123
database.url = jdbc:mysql://localhost/database
username = dba


Da XML a JavaBean
Talvolta si possono creare situazioni dove è necessario trasformare il contenuto di un documento XML in uno o più JavaBean ed anche in questo caso Digester di rivela essere un ottimo aiuto, facilitando lo sviluppo.
Come in precedenza partiamo da un semplice documento XML a rappresentare un piccolo archivio di film, per i quali sono indicati un codice, un titolo e la durata in minuti.

<?xml version="1.0"?>
<archivio>
<film>
<codice> 123 </codice>
<titolo> Una pura formalita' </titolo>
<durata> 108 </durata>
</film>
<film>
<codice> 124 </codice>
<titolo> Office Space </titolo>
<durata> 89 </durata>
</film>
<film>
<codice> 125 </codice>
<titolo> Swimming with sharks </titolo>
<durata> 101 </durata>
</film>
</archivio>

Dal punto di vista Java possiamo rappresentare il film attraverso un piccolo JavaBean, il cui codice può essere più o meno questo:

public class Film {

  // Attributi

  private long codice;
  private String titolo;
  private int durata;


  // Costruttori

  public Film() {
    // costruttore vuoto
  }

  public Film(long codice, String titolo, int durata) {
    setCodice(codice);
    setTitolo(titolo);
    setDurata(durata);
  }


  // Getters & Setters

  public void setCodice(long arg) {
    codice = arg;
  }


  public long getCodice() {
    return codice;
  }

  public void setTitolo(String arg) {
    titolo = arg;
  }

  public String getTtiolo() {
    return titolo;
  }

  public void setDurata(int arg) {
    durata = arg;
  }

  public int getDurata() {
    return durata;
  }


  // Questo metodo verrà utilizzato per testare il nostro oggetto
  public String toString() {
    StringBuffer retBuff = new StringBuffer();
    retBuff.append("Codice=").append(codice).append(",");
    retBuff.append("Titolo=").append(titolo).append(",");
    retBuff.append("Durata=").append(durata);
    return retBuff.toString();
  }
}

A completare l'esempio, possiamo creare un secondo oggetto, che chiamiamo "Archivio" che avrà il compito di contenere la lista dei film. Per semplificare le operazioni, così come in precedenza, inseriremo all'interno di questo oggetto sia il codice main per il testing, sia quello per effettuare il parsing. In questo caso, però, utilizzeremo una logica differente, per mostrare un ulteriore aspetto di Digester.

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


public class Archivio {

  private List listaFilm;

  // Il costruttore inizializzerà la lista
  public Archivio() {
    listaFilm = new ArrayList();
  }


  // Questo metodo aggiungerà un nuovo Film alla lista
  public void addFilm(Film film) {
    listaFilm.add(film);
  }

  // Questo metodo verrà utilizzato per testare il nostro oggetto
  public String toString() {
    StringBuffer retBuff = new StringBuffer();
    for (int i=0; i<listaFilm.size(); i++) {
      retBuff.append(listaFilm.get(i));
      retBuff.append(System.getProperty("line.separator"));
    }
    return retBuff.toString();
  }
}

Fino a qui nulla di strano. Il nostro oggetto Archivio contiene solo un oggetto List che conterrà, a sua volta, tutti i JavaBean di tipo "Film" che verranno creati tramite il Digester.
Il metodo di parsing sarà statico e ritornerà una istanza dell'oggetto Archivio, costruito in base al contenuto del documento XML:

public static Archivio parse(String filename) throws IOException, SAXException {

  // Creazione oggetto Digester
  Digester digester = new Digester();

Dopo aver creato l'oggetto Digester, non inseriremo nulla all'interno dello stack, ma definiremo subito una regola il cui compito sarà quello di inizializzare una nuova istanza della classe Archivio quando Digester incontrerà il tag "archivio":

  // Creazione oggetto Archivio
  digester.addObjectCreate("archivio", Archivio.class);

All'interno del nostro documento avremo una o più tag contenenti le informazioni necessarie per la creazione dei JavaBean di tipo Film. Il compito di Digester sarà, quindi, quello di instanziare un oggetto Film (quando incontra il tag di apertura "archivio/film"), settarne le proprietà, (tramite "archivio/film/codice", " archivio/film/titolo" e " archivio/film/durata") ed infine inserire questo oggetto all'interno della collezione. Ecco un possibile modo per realizzare questo:

   // Creazione oggetto Archivio
  digester.addObjectCreate("archivio/film", Film.class);

  // Inserimento proprietà attraverso i setters
  digester.addBeanPropertySetter("archivio/film/codice", "codice");
  digester.addBeanPropertySetter("archivio/film/titolo", "titolo");
  digester.addBeanPropertySetter("archivio/film/durata", "durata");

  // Inserimento oggetto Film all'interno della lista
  digester.addSetNext("archivio/film", "addFilm" );

Il metodo chiave di questa soluzione è "addBeanPropertySetter" che trasferisce all'interno del bean il valore contenuto nel tag indicato, dopo aver trasformato il valore di tipo String letto dal documento XML, nel tipo richiesto dal setter.
A questo punto effettueremo il parsing del documento attraverso lo stesso metodo parse visto in precedenza utilizzando l'oggetto che ci verrà restituito, in questo caso di tipo Archivio, come valore di ritorno del nostro metodo:

   return (Archivio)digester.parse(filename);
 }

Per completare l'esempio basta creare il metodo main di test:

public static final void main(String[] args) {
  try {
  
  Archivio archivio = Archivio.parse(args[0]);
  
  System.out.println();
  
  System.out.println(archivio.toString());
  }
  catch (Exception e) {
  
  e.printStackTrace();
  }
}


Funzionalità avanzate
Nei due esempi, piuttosto elementari, che abbiamo proposto, abbiamo notato come tutte le operazioni di parsing si basano su regole che vengono inserite all'interno del codice. Sebbene nella maggior parte dei casi questo possa essere sufficiente, vi sono situazioni nelle quali può essere necessario poter applicare delle regole che sono definite da un file di configurazione esterno, aumentando la portabilità del codice.
Digester, ovviamente, permette di costruire un sistema che si possa "configurare" piuttosto che "programmare", attravero l'utilizzo del package org.apache.commons.digester.xmlrules.



Figura 2 - Il package xmlrules per la configurazione esterna delle regole

Per definire le regole si utilizzerà, ovviamente, un documento XML, il cui DTD, "digester-rules.dtd" è inserito all'interno del package. All'interno del codice l'oggetto Digester verrà inizializzato attraverso il metodo statico "createDigester" dell'oggetto "DigesterLoader", che si occuperà di leggere le regole definite nel documento XML. Fatto questo potremo chiamare il metodo "parse", così come abbiamo visto negli esempi precedenti, per effettuare le operazioni di parsing sui nostri file XML.


Figura 3 - Il package per gestire documenti con il formato RSS

XML, tra le altre cose, è anche alla base di Rich Site Summary (RSS), il formato che sta diventando uno standard per i newsfeed presenti in rete. Tra le API di Digester esiste il package org.apache.commons.digester.rss, il cui scopo è proprio quello di aiutare il programmatore ad accedere a questi documenti e, quindi, utilizzarli all'interno delle proprie applicazioni.

Digester, però, non si esaurisce qui, soprattutto per quello che concerne il discorso dulle regole. Il componente del progetto Jakarta Commons, infatti, permette allo sviluppatore di personalizzare il sistema costruendo le proprie regole, attraverso l'estensione della classe Rule.

 

Conclusioni
Questo, però, non significa che Digester debba essere adottato come la soluzione finale a tutti i problemi legati all'integrazione di XML all'interno dei nostri progetti. Di soluzioni alternative ce ne sono e può darsi che per alcune situazioni specifiche queste si possano rivelare più efficienti rispetto a questo componente o che forniscano maggiori funzionalità generiche, come, ad esempio, la possibilità di generazione di documenti XML partendo dal codice.
Il grosso vantaggio di Digester, però, è la sua grossa versatilità, che non toglie nulla né alla potenza e tantomeno alla semplicità d'utilizzo, proponendosi come uno strumento da tenere sempre a portata di mano... sono certo che quando inizierete ad usarlo, lo userete più di quanto avevate preventivato.

Link e risorse
[1] Erik Swenson - "Usimplify XML file processing with the Jakarta Commons Digester", JavaWorld.com, 2002
[2] Philipp K. Janert - "Learning and Using Jakarta Digester", OnJava.com, 2002

 
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