Nei precedenti articoli JAXB: l’XML in Java e JAXB: customizzazione del binding si è introdotto il framework JAXB (Java Architecture for XML Binding), un framework a supporto dell'utilizzo dell'XML in Java. Nel primo articolo si sono descritti i principali strumenti (bind, marshal e unmarshal). Nel secondo si è visto come sia possibile customizzare la compilazione degli schemi al fine di adeguarla alle proprie esigenze o per compilare schemi altrimenti non compilabili.
In questo articolo si approfondirà ulteriormente la conoscenza del framework affrontando due argomenti. Il primo descrive come utilizzare JAXB nel caso di schemi XML che facciano uso di un substitutionGroup
, strumento che permette di includere nei documenti XML elementi non noti a priori ma generati a partire da una matrice comune. Nel secondo si introdurrà la validazione, mostrando come applicarla e come intervenire nel processo di validazione per determinarne il comportamento. Vedremo inoltre come possa risultare utile o necessario cambiare il namespace o il nome degli elementi.
L'articolo è costituito dalle seguenti sezioni:
- Precondizioni
- Schemi XML, compilazione e impostazione di un progetto JAVA
- SubstitutionGroup in JAXB
- La validazione
Per eseguire gli esempi proposti, assicurarsi di avere correttamente installato il Java Development Kit (a partire da JDK 6). In allegato all’articolo un archivio zip con gli esempi descritti di seguito (un progetto Eclipse e gli schemi XML).
Schemi XML, compilazione e impostazione di un progetto JAVA
Vengono presentati di seguito i due schemi usati per lo svolgimento degli esempi. Da notare nello schema Abbigliamento.xsd la presenza di un substitutionGroup
che ci consente di specificare nell'elemento "Persona" la possibilità di avere diversi capi d'abbigliamento, senza dovere specificare quali (mantenendo pertanto un'elevata capacità d'evoluzione).
Abbigliamento.xsd
<?xml version="1.0" encoding="ISO-8859-1" ?>
<xs:schema
xmlns:sc1="http://www.abbigliamento.com"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.abbigliamento.com"
>
<xs:complexType name="capoAbbigliamento">
<xs:sequence>
<xs:element ref="sc1:abbigliamento"/>
</xs:sequence>
</xs:complexType>
<xs:element name="abbigliamento" type="sc1:tipoAbbigliamento" abstract="true"/>
<xs:complexType name="tipoAbbigliamento" abstract="true">
<xs:sequence>
<xs:element name="taglia" type="xs:string"/>
<xs:element name="tessuto" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:element name="maglietta" type="sc1:tipoMaglietta" substitutionGroup="sc1:abbigliamento"/>
<xs:element name="pantaloni" type="sc1:tipoPantaloni" substitutionGroup="sc1:abbigliamento"/>
<xs:element name="giacca" type="sc1:tipoGiacca" substitutionGroup="sc1:abbigliamento"/>
<xs:complexType name="tipoMaglietta">
<xs:complexContent>
<xs:extension base="sc1:tipoAbbigliamento">
<xs:sequence>
<xs:element name="collo" type="xs:string"/>
<xs:element name="maniche" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tipoPantaloni">
<xs:complexContent>
<xs:extension base="sc1:tipoAbbigliamento">
<xs:sequence>
<xs:element name="tipo" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tipoGiacca">
<xs:complexContent>
<xs:extension base="sc1:tipoAbbigliamento">
<xs:sequence>
<xs:element name="bavero" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:schema>
Persona.xsd
<?xml version="1.0" encoding="ISO-8859-1" ?>
<xs:schema
xmlns:sc1="http://www.abbigliamento.com"
xmlns:sc2="http://www.persona.com"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="www.persona.com"
>
<xs:import namespace="http://www.abbigliamento.com" schemaLocation="../Schemi/Abbigliamento.xsd" />
<xs:element name="Persona">
<xs:complexType>
<xs:sequence>
<xs:element name="nome" type="xs:string" />
<xs:element name="cognome" type="xs:string" />
<xs:element name="vestiario" type="sc1:capoAbbigliamento" maxOccurs="3" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
La compilazione avviene come mostrato negli articoli precedenti tramite il binding compiler xjc.
Possiamo importare le classi generate dal binding compiler in un progetto Java (nell’esempio ProgettoPersona
), aggiungendo un package (ad esempio com.mains
) per ospitare le classi di test e uno per le utilities di JAXB (marshalling, unmarshalling), già introdotte nei precedenti articoli. Otterremo una struttura simile a quella mostrata nell'immagine seguente.
SubstitutionGroup in JAXB
Gli schemi XML ci permettono di prevedere elementi non obbligatori, in numero non determinato, o di un tipo non predeterminato. In JAXB i primi casi non presentano complicazioni; possiamo istanziare questi oggetti o invocarli partendo dagli oggetti che li contengono senza particolari difficoltà. Complicazioni che invece sorgono nel momento in cui si lavora con schemi che presentano elementi non noti a priori ma generati a partire da una matrice comune.
Un SubstitutionGroup consente di specificare elementi che possono rimpiazzarne un altro, un tipo base che può essere astratto. Gli elementi che verranno effettivamente utilizzati saranno dello stesso tipo dell'elemento rimpiazzabile (se non è astratto), o di tipo derivato. Come è possibile vedere nell'esempio presentato nel capitolo precedente, basta indicare la presenza di generici capi d'abbigliamento. In definitiva, un substitutionGroup
permette di costruire una collezione di elementi (maglioni, calze, …) specificati a partire da un elemento generico (il capo d'abbigliamento).
Data l'utilità, l'uso dei substitutionGroups
è pertanto diffuso (basti vedere l'utilizzo intensivo che se ne fa negli schemi dell'Open Geospatial Consortium) e può permettere di risparmiare tempo nella realizzazione di grosse collezioni di schemi.
Popolamento di oggetti appartenenti a un substitutionGroup
Nel nostro caso, la classe "Persona", generata dal compilatore xjc, offrirà i metodi di Get
e Set
per gli attributi "Nome" e "Cognome" e una Get
per la lista del vestiario, che richiederà come ingressi oggetti della classe CapoAbbigliamento
. Fin qui niente di nuovo. Il problema sorge nel momento in cui occorrerà passare dei valori alla lista del vestiario.
Istanziato un oggetto della classe CapoAbbigliamento
, provando ad invocare una setAbbigliamento
, ci accorgeremo che dovremo passare un oggetto di tipo
JAXBElement<? Extends TipoAbbigliamento>
Nell'immagine seguente si mostra come l'IDE Eclipse ci suggerisce di invocare il metodo.
Detto altrimenti, se ci aspettavamo di poter passare direttamente un oggetto del tipo TipoGiacca
, TipoMaglietta
o TipoPantaloni
resteremo delusi. Occorrerà invece utilizzare i metodi offerti dall'ObjectFactory del package com.abbigliamento
per poter istanziare degli oggetti JAXB intermedi, dei wrapper, che andranno ad incapsulare gli oggetti veri e propri che vorremmo passare.
Vediamo come:
TipoMaglietta maglietta = new TipoMaglietta();
maglietta.setCollo("V");
maglietta.setManiche("Corte");
maglietta.setTaglia("M");
maglietta.setTessuto("Cotone");
JAXBElement<TipoMaglietta> elemMaglietta = abbigliamentoFactory.createMaglietta(maglietta);
CapoAbbigliamento capo = new CapoAbbigliamento();
capo.setAbbigliamento(elemMaglietta);
persona.getVestiario().add(capo);
Una volta istanziato un oggetto maglietta
, viene utilizzato un metodo offerto dalla factory, cha a sua volta istanzia un oggetto JAXBElement
, in sostanza un wrapper che possiamo parametrizzare. E proprio uno di questi elementi parametrizzati deve essere utilizzato per popolare l'oggetto istanza della classe CapoAbbigliamento
.
Si riporta di seguito un esempio di come popolare la classe Persona
, per poi stampare a video l'XML corrispondente, utilizzando per la stampa la classe StampGenericXML
già presentata nell'articolo JAXB l'XML in Java (l'esempio completo è nell'allegato).
Persona persona = new Persona();
persona.setNome("Pippo");
persona.setCognome("De Pippis");
ObjectFactory abbigliamentoFactory = new ObjectFactory();
TipoGiacca giacca = new TipoGiacca();
giacca.setBavero("classico");
giacca.setTaglia("M");
giacca.setTessuto("Cotone");
JAXBElement<TipoGiacca> elemGiacca = abbigliamentoFactory.createGiacca(giacca);
TipoMaglietta maglietta = new TipoMaglietta();
maglietta.setCollo("V");
maglietta.setManiche("Corte");
maglietta.setTaglia("M");
maglietta.setTessuto("Cotone");
JAXBElement<TipoMaglietta> elemMaglietta = abbigliamentoFactory.createMaglietta(maglietta);
TipoPantaloni pantaloni = new TipoPantaloni();
pantaloni.setTipo("Zuava");
pantaloni.setTaglia("M");
pantaloni.setTessuto("Cotone");
JAXBElement<TipoPantaloni> elemPantaloni = abbigliamentoFactory.createPantaloni(pantaloni);
CapoAbbigliamento capo1 = new CapoAbbigliamento();
capo1.setAbbigliamento(elemGiacca);
CapoAbbigliamento capo2 = new CapoAbbigliamento();
capo2.setAbbigliamento(elemMaglietta);
CapoAbbigliamento capo3 = new CapoAbbigliamento();
capo3.setAbbigliamento(elemPantaloni);
persona.getVestiario().add(capo1);
persona.getVestiario().add(capo2);
persona.getVestiario().add(capo3);
String context="com.persona";
StampGenericXML.staticStampGenericXML(persona, context);
Lettura da oggetti appartenenti a un substitutionGroup
Abbiamo visto finora il popolamento della classe Persona
. In lettura avremo un problema analogo in quanto il metodo getAbbigliamento
restituisce un elemento di tipo
JAXBElement<? extends TipoAbbigliamento>
Ancora una volta avremo bisogno di una serie di passaggi intermedi per arrivare ad ottenere l'oggetto in questione. In questo caso, la Get
restituirà un wrapper, dal quale sarà possibile ottenere l'oggetto tramite una getValue
. L'oggetto restituito sarà un generico oggetto JAXBElement
, occorrerà pertanto effettuare un cast verso il tipo corretto.
Ma di norma non conosceremo il tipo dell’oggetto in questione, per cui probabilmente ci converrà utilizzare il comando instanceof
per individuare la giusta classe tra quelle possibili ed effettuare l'operazione di cast di conseguenza. Seguirà un esempio di come può avvenire il riconoscimento del tipo corrispondente e il successivo uso.
for(int i=0; i<persona.getVestiario().size();i++){
CapoAbbigliamento capo = persona.getVestiario().get(i);
if(capo.getAbbigliamento().getValue() instanceof TipoGiacca){
TipoGiacca giaccaOut = (TipoGiacca)
capo.getAbbigliamento().getValue();
System.out.println("Giacca: "+giaccaOut.getBavero());
}
else if(capo.getAbbigliamento().getValue() instanceof TipoMaglietta){
TipoMaglietta magliettaOut = (TipoMaglietta)
capo.getAbbigliamento().getValue();
System.out.println("Maglietta:
"+magliettaOut.getTessuto());
}
else if(capo.getAbbigliamento().getValue() instanceof TipoPantaloni){
TipoPantaloni pantaloniOut = (TipoPantaloni)
capo.getAbbigliamento().getValue();
System.out.println("Pantaloni: "+pantaloniOut.getTipo());
}
}
La validazione
Nell'esempio precedente abbiamo visto come popolare la classe con alcuni capi d'abbigliamento. Nulla ci vieta di aggiungere un quarto capo, ad esempio un'altra maglietta, e chiedere la stampa a video con la solita operazione. Tutto procede al meglio e, come mostrato nel main d'esempio che si può trovare nell'archivio allegato, tutto fila liscio. Ma proprio qui sorge il problema.
Nello schema di partenza avevamo imposto che una persona possa avere al massimo tre capi d'abbigliamento, mentre siamo arrivati ad aggiungerne un quarto senza che succedesse niente. Com'è stato possibile?
Semplicemente, ciò avviene perché in fase di marshalling
, come del resto di unmarshalling
, avviene un controllo sui tipi e sul loro ordine, ma non sul numero di elementi. Detto altrimenti, queste operazioni non costituiscono una validazione, se volessimo associare ad esse la validazione dovremmo specificarlo. Vedremo ora come.
Impostare lo schema
Se volessimo effettuare una validazione non basterebbero le classi generate dal binding compiler, occorrerà invece rendere disponibili gli schemi XML da cui siamo partiti. Per farlo, possiamo aggiungere al progetto una cartella, la poniamo nella cartella src
e la chiamiamo META-INF
, popolandola con gli schemi nel rispetto della struttura originaria, in modo che i riferimenti contenuti negli schemi continuino a risultare validi. E' buona norma porre tutto in una cartella xsds
. L’immagine seguente mostra la situazione attesa.
Per la validazione JAXB fa affidamento su due classi:
javax.xml.validation.Schema
javax.xml.validation.SchemaFactory
Un'istanza della classe Schema
, la chiameremo comunemente "schema" (in corsivo per evidenziare la differenza con gli schemi XML), conterrà le informazioni necessarie per la validazione, mentre la classe SchemaFactory
fornirà i metodi per popolare le informazioni a partire dagli schemi XML.
Queste informazioni possono essere passate in diversi modi. Alcuni di questi prevedono l'utilizzo di una classe ResourceLocator
, altri solamente il passaggio di un file, uno schema XML capofila.
In generale, è possibile dire che sarà possibile popolare lo schema a partire dalle classi Java File
, URL
e Source
. In particolare, l'ultima modalità presenta l'utilità aggiuntiva di permettere di passare più di una sorgente, modalità particolarmente utile quando occorre ricostruire collezioni di schemi XML che dispongano di più di uno schema XML capofila.
Di seguito si riporta la classe ValidazioneSchema
che offre un metodo, getSchema
, per restituire lo schema popolato nel costruttore.
package com.mains;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXException;
public class ValidazioneSchema {
private Schema schema = null;
public ValidazioneSchema(String pathToSchema, int scelta){
SchemaFactory fct = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
try {
//1. file
if(scelta==1){
File schemaFile = new File(pathToSchema);
this.schema = fct.newSchema(schemaFile);
}
//2. url
if(scelta==2){
URL url;
try {
url = new URL(this.getClass().getResource(pathToSchema).toString());
schema = fct.newSchema(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
//3. Source
if(scelta==3){
Source schemaSource = new StreamSource(this.getClass().getResourceAsStream(pathToSchema), this.getClass().getResource(pathToSchema).toString());
schema = fct.newSchema(schemaSource);
}
}catch (SAXException e1) {
e1.printStackTrace();
}
}
public Schema getSchema(){
return this.schema;
}
}
Uno dei parametri di ingresso del costruttore, scelta
, consente di scegliere una delle tre modalità presentate. Nel main d'esempio si utilizzerà la prima scelta e lo schema verrà caricato da File
.
La controindicazione di questa scelta è che se gli schemi XML in questione sono remoti o si trovano in un archivio, ad esempio un jar, non sarà possibile passarli come File
ma si dovrà ricorrere a una delle altre scelte, URL
o Source
. In quest’ultimo caso la risorsa è acquisita utilizzando il class loader, il che implica che la risorsa dovrà essere nel classpath per essere acquisita. Nell'esempio abbiamo posto la cartella META-INF
nella cartella source
, pertanto sarà acquisibile.
E’ possibile utilizzare le modalità URL
o Source
nell'esempio proposto passando la stringa:
/META-INF/xsds/Persona/All/Persona.xsd
Altro aspetto da considerare è la posizione effettiva degli schemi XML. Se puntano a schemi remoti, occorrerà tener presente che i tempi di collezione delle informazioni da caricare nello schema saliranno, anche significativamente. E’ pertanto utile avere in locale tutti gli schemi XML (e assicurarsi che i relativi riferimenti puntino solo a schemi locali).
Stampa con validazione
Siamo ora finalmente pronti a utilizzare la validazione. Per aggiungere la validazione ai processi di marshalling
o unmarshalling
occorrerà utilizzare l'apposito metodo setSchema
, fornito dalle classi Marshaller
e Unmarshaller
.
Per sperimentarlo, possiamo aggiungere il seguente metodo alla classe StampGenericXML
:
static public void staticStampGenericXMLWithValidation(Object objectJAXB, String context, Schema schema){
try {
JAXBContext jaxbLocalContext = JAXBContext.newInstance (context);
Marshaller marshaller = jaxbLocalContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setSchema(schema);
marshaller.marshal(objectJAXB, System.out);
} catch (JAXBException e1) {
e1.printStackTrace();
}
}
Rispetto al precedente metodo, cambierà solamente il fatto di avere un parametro di ingresso in più, schema
, utilizzato per popolare il corrispondente metodo offerto dal marshaller
.
Se provassimo a lanciare la stampa a video con questo nuovo metodo otterremo un'eccezione e nel relativo output potremo leggere
cvc-complex-type.2.4.d: Invalid content was found starting with element 'ns3:Persona'. No child element '{vestiario}' is expected at this point.
Ossia, vi è un capo di vestiario di troppo, con conseguente blocco della stampa a video. Se invece della stampa a video avessimo chiesto una stampa su file, o una delle operazioni di unmarshalling
, avremmo ottenuto lo stesso risultato, l'annullamento dell’operazione.
Personalizzare la gestione degli errori
Abbiamo visto come aggiungere la validazione e come essa si comporta nel caso in cui l'XML non soddisfi il relativo schema XML. Il comportamento di default prevede l'arresto dell’operazione. Può essere però utile modificare questo comportamento, ad esempio permettendo che la stampa a video avvenga comunque ma visualizzando al contempo un messaggio di warning. O potrebbe risultare utile differenziare la reazione a seconda della gravità dell'infrazione rilevata.
A tale scopo è possibile aggregare al Marshaller
o al suo opposto un EventHandler
.
Aggiungiamo quindi alla classe StampGenericXML
un nuovo metodo, getEventHandler
:
static private ValidationEventHandler getEventHandler() {
ValidationEventHandler handler = new ValidationEventHandler() {
public boolean handleEvent(ValidationEvent ve) {
ValidationEventLocator vel = ve.getLocator();
System.out.println("["+vel.getColumnNumber()+":"+vel.getLineNumber()+"] Event: " + ve.getMessage());
if (ve.getSeverity() == ValidationEvent.WARNING) {
System.out.println("Event severity: WARNING!");
}
else if(ve.getSeverity() == ValidationEvent.ERROR){
System.out.println("Event severity: ERROR!");
}
else if(ve.getSeverity() == ValidationEvent.FATAL_ERROR){
System.out.println("Event severity: FATAL ERROR!");
}
return true;
}
};
return handler;
}
Con questo metodo è possibile ottenere che l'operazione in corso prosegua, pur stampando a video due messaggi. Il primo specifica posizione e tipo dell'errore, il secondo informa circa la severità dell'errore rilevato. Nel caso dell'esempio precedente, il grado di severità è fatale. Sono previsti tre gradi di severità: in base alle raccomandazioni emanate dal W3C (W3C XML 1.0 Recommendation): warning, errore ed errore fatale. Il warning non ha una definizione stretta e può variare a seconda delle implementazioni della specifica JAXB.
Per aggiungere la gestione delle eccezioni si procede in modo del tutto analogo all'aggiunta dello schema al Marshaller
(o al suo opposto) utilizzando il metodo setEventHandler
. Negli esempi disponibili nell'archivio allegato all'articolo l'EventHandler
viene impostato se richiesto dall'oggetto che invoca il metodo di stampa a video, tramite una variabile booleana (handler
):
if(handler) marshaller.setEventHandler(getEventHandler());
Modifica del namespace o del nome di un elemento
In alcuni casi può risultare necessario o utile utilizzare un namespace diverso da quello di partenza. Potremmo trovarci ad esempio nella situazione di dovere validare un oggetto che appartiene a un namespace, ma per il quale la validazione ne richiede un altro. Altra situazione che si può verificare è che la classe generata non abbia una root element (annotazione @XmlRootElement
) e occorrerà fornirglielo per poter effettuare operazioni di marshalling.
In questo caso, se volessimo utilizzare l'XML per i nostri scopi, dovremmo impiegare un oggetto wrapper per istanziare l’oggetto voluto. L'esempio seguente si mostra:
Persona fortunato = new Persona();
fortunato.setCognome("Dei Paperoni");
fortunato.setNome("Gastone");
QName qname = new QName("http://prova.rinomina/test", "Fortunato");
JAXBElement<com.persona.Persona> individuoJAXB = new JAXBElement<com.persona.Persona>(qname, com.persona.Persona.class, fortunato);
System.out.println("nn*** Namespace modificato:");
StampGenericXML.staticStampGenericXML(individuoJAXB, context);
System.out.println("nn*** Namespace modificato, validazione e datahandler:");
StampGenericXML.staticStampGenericXMLWithValidation(individuoJAXB, context, schema, true);
La classe QName
presenta nel costruttore invocato due voci, la prima per impostare il namespace e la seconda per il nome dell'elemento.
Da notare che, dopo una simile modifica, l'oggetto non supererebbe la validazione fatta con lo schema precedente.
Conclusioni
Nell'articolo abbiamo approfondito due aspetti che gravitano nell'orbita del framework JAXB. Abbiamo visto come e perché utilizzare un substitutionGroup
, strumento potente ma che richiede un po' d'attenzione per essere utilizzato efficacemente.
Successivamente abbiamo approfondito la validazione e alcuni aspetti connessi. JAXB permette di effettuare la validazione, ma di default non avviene. Abbiamo visto come associare il processo di validazione al marshalling o all'unmarshalling, mostrando come acquisire gli schemi XML e come gestire le eccezioni sollevate in fase di validazione, diversificando la reazione in base alla severità del problema riscontrato.
Si è infine accennato a come modificare il namespace o il nome di un elemento per ovviare a problemi che possono sorgere durante la validazione o più semplicemente per semplificare il riuso di classi già esistenti.