|
Introduzione
Il protocollo applicativo definito nella prima parte
di questa miniserie, definisce i comandi basilari per
il funzionamento della chat e la loro implementazione
tramite messaggi SOAP. Per utilizzare in pratica questi
messaggi é necessario implementare un programma
Java che li generi e che ne consenta l'interpretazione
(parsing) per poterne estrarre i valori desiderati.
Nella prima parte abbiamo identificato un paio di dettagli
tecnici che verranno ora utilizzati: l'utilizzo di JAXM
(Java API for XML Messagiung) come API per la messaggistica
SOAP (si veda [5]) ed il riutilizzo del codice server
della chat originale a cui questo progetto si ispira
(si veda [1], [2] e [3]).
Un
po' di JAXM
Vista l'esigenza di produrre e consumare un gran numero
di messaggi SOAP, é opportuno racchiudere il
codice che conosce i dettagli della costruzione e fruizione
dei messaggi SOAP in un unico punto. In questo modo,
dal livello XML/SOAP, le informazioni vengono trasformate
in dati Java, sotto forma di chiamate a metodi e valori
in formato primitivo. E' infatti scomodo sparpagliare
il codice che tratta con XML/SOAP per tutta l'applicazione
e tra l'altro produce una implementazione meno manutenibile.
Separando la logica applicativa da quella di comunicazione,
il livello XML/SOAP é racchiuso solo in quest'ultima
parte che sarà dunque la sola che avrà
cognizione dell'API legata a SOAP (in questo caso JAXM).
Per astrarre il livello di trasporto dalla logica applicativa
é possibile procedere in due modi: il modo più
complesso - ma che produce la soluzione più pulita
- é quella di creare un object model che rappresenta
le entità coinvolte. In questo caso si potrebbe
avere:
Listato
1
//l'indirizzo IP é gestito internamente all'implementazione
public interface ChatProxy {
//ritorna il token
public String login( String user ) throws
ChatException;
public String[] getMessages() throws ChatException;
public void sendMessage( String message
) throws ChatException;
public void logout() throws ChatException;
}
Oppure
é possibile codificare una classe di utilità
che contenga i singoli metodi che creano ed elaborano
i messaggi SOAP. Eventualmente l'implementazione dell'interfaccia
ChatProxy potrebbe fare uso appunto di questa classe
di utilità, oppure implementare direttamente
le API JAXM. Quest'ultima opzione non é delle
più agevoli in quanto é necessario molto
codice per poter creare messaggi SOAP: tutta la struttura
del messaggio é infatti modellata con un object
model specifico.
Qui si é utilizzato l'approccio più semplice,
non realizzando il proxy ma limitandosi alla sola classe
di utilità. Il listato completo é accessibile
dal link in fondo all'articolo, mentre nel listato 2
é possibile vedere alcuni passaggi significativi.
Listato
2
public class Utils {
public static SOAPMessage getLoginMessage(String
user,
String token )
throws
SOAPException{
return getMessage( user, token,
"Login", null );
}
public
static SOAPMessage getMessage(String user,
String
token, String tag,
String
tagValue)
throws SOAPException {
MessageFactory mf = MessageFactory.newInstance();
SOAPMessage msg = mf.createMessage();
SOAPPart sp = msg.getSOAPPart();
SOAPEnvelope env = sp.getEnvelope();
SOAPHeader hdr = env.getHeader();
SOAPBody bdy = env.getBody();
if(
user != null && token != null )
fillHeader( env,
hdr, user, token );
else
env.addNamespaceDeclaration("mbc",
myURI);
SOAPBodyElement
gltp;
gltp= bdy.addBodyElement(env.createName(tag,
"mbc", myURI));
if(
tagValue != null ) {
gltp.addTextNode(
tagValue );
}
return
msg;
}
protected
static void fillHeader(SOAPEnvelope env,
SOAPHeader hdr,
String
user,
String token)
throws
SOAPException {
String myIP = "(unknown)";
try
{
InetAddress na =
InetAddress.getLocalHost();
myIP = na.getHostAddress();
}
catch( UnknownHostException
ex1 ) {
}
env.addNamespaceDeclaration("mbc",
myURI);
hdr.addHeaderElement(env.createName("User",
"mbc", myURI)).addTextNode(
user );
hdr.addHeaderElement(env.createName("Token",
"mbc",
myURI)).addTextNode( token );
hdr.addHeaderElement(env.createName("IP",
"mbc",
myURI)).addTextNode(
myIP );
}
}
L'implementazione
si basa sul metodo getMessage() che si occupa sostanzialmente
di generare un messaggio SOAP utilizzando JAXM caricandolo
con i dati forniti come parametro. Il metodo utilizza
inoltre fillHeader() che si preoccupa di valorizzare
i soli dati del tag Header.
L'utilizzo ripetuto del metodo getMessage() da parte
degli altri metodi della classe consente di riutilizzare
il classico codice JAXM per la creazione del messaggio
senza doverlo reimplementare per ogni flusso SOAP utilizzato.
Discorso differente per quanto riguarda l'operazione
"GetMessages". Se da una parte quasi tutti
i messaggi hanno una struttura semplice, composta da
un solo tag XML di input ed uno di output (come Login,
Logout, SendMessage), l'operazione "GetMessages"
ritorna una lista di tag e dunque richiede due metodi
più complessi: uno per la codifica del messaggio
SOAP ed uno per la decodifica.
Questi due metodi sono implementati nella classe Utils
come getGetMessagesResponse() ed extractMessages() -
si veda il Listato 3.
Come si evince dall'osservazione del codice, i tag con
i messaggi sono elementi SOAPElement figli di SOAPBody.
La navigazione nell'object model JAXM avviene con i
classici addChildElement() per l'aggiunta e getChildElement()
per la lettura, in modo similare alle JAXP (Java API
for XML Processing).
Listato
3
public class Utils {
public static SOAPMessage getGetMessagesResponse(String[]
msgs)
throws
SOAPException {
MessageFactory mf = MessageFactory.newInstance();
SOAPMessage msg = mf.createMessage();
SOAPPart sp = msg.getSOAPPart();
SOAPEnvelope env = sp.getEnvelope();
SOAPHeader hdr = env.getHeader();
SOAPBody bdy = env.getBody();
env.addNamespaceDeclaration("mbc",
myURI);
SOAPBodyElement
gltp;
gltp= bdy.addBodyElement(env.createName("GetMessagesResponse",
"mbc",
myURI));
if(
messages != null ) {
for( int i=0; i<messages.length;
i++ ) {
SOAPElement
e = gltp.addChildElement(env.createName(
"Message",
"mbc", myURI));
String
id = "000"+(i+1);
id =
id.substring( id.length()-3 );
e.addAttribute(
env.createName("id", "mbc", myURI
), id);
e.addTextNode(
messages[i] );
}
}
return
msg;
}
public
static String[] extractMessages(SOAPMessage msg )
throws
SOAPException {
List list = new LinkedList();
SOAPPart sp = msg.getSOAPPart();
SOAPEnvelope env = sp.getEnvelope();
SOAPBody bdy = env.getBody();
Iterator
childs = bdy.getChildElements();
SOAPElement e1 = (SOAPElement)childs.next();
if( e1.getElementName().getLocalName()
.equals("GetMessagesResponse")){
childs = e1.getChildElements();
while(
childs.hasNext() ) {
SOAPElement
e = (SOAPElement)childs.next();
Iterator
ch = e.getChildElements();
Text
text = (Text)ch.next();
list.add(
text.getValue() );
}
}
//Trasforma
la lista in un array
String[] m = new String[ list.size()
];
for( int i=0; i<m.length;
i++ )
m[i] = (String)list.get(
i );
return
m;
}
}
Riutilizzo del codice
Una volta preparato il codice basilare per la comunicazione
SOAP é necessario integrarlo con quello più
prettamente legato alla gestione della Chat. Come si
diceva nella scorsa puntata, se da una parte la classe
ChatBuffer può essere riutilizzata completamente,
NickRegister dovrà essere leggermente modificata.
Listato
4
class NickRegister{
// I metodi send() e replace() non vengono
utilizzati
// in questa versione di Chat SOAP
public
synchronized void talk(String from, String msg){
ChatBuffer cb;
Enumeration
e = register.elements();
while(e.hasMoreElements()){
cb = (ChatBuffer)e.nextElement();
cb.put("["
+ from + "] " + msg);
}
}
public
synchronized String[] getMessages(String s) {
ChatBuffer cb = (ChatBuffer)register.get(
s );
return cb.get();
}
}
Nel
listato 4 si può vedere un estratto della classe
NickRegister. La prima modifica riguarda il metodo talk()
che si occupa di inserire un messaggio nella coda degli
utenti. In questa versione, viene indicato anche il
nickname di chi ha inviato il messaggio, racchiuso tra
parentesi quadre.
La seconda novità é il metodo getMessages().
Questo metodo si aspetta un nickname e restituisce un
array di stringhe con i messaggi presenti nel ChatBuffer
del nickname specificato. L'implementazione non é
altro che un proxy sulla classe ChatBuffer, ma consente
di evitare di tenere traccia del ChatBuffer a livello
del controller principale. Come si vedrà infatti
nel prossimo paragrafo, il cuore della Chat avrà
solo il riferimento al NickRegister in uso.
La Servlet JAXM
Il server della Chat é implementato da una Servlet
ed infatti gira all'interno di un Web Container. Questa
é una prassi standard per le applicazioni JAXM
in quanto con questa tecnologia i server SOAP/HTTP sono
implementati appunto come Servlet che derivano da JAXMServlet.
Il metodo onMessage() della Servlet, che si occupa di
elaborare il messaggio SOAP in arrivo, non é
altro che un grosso "switch" che opera sul
nome dell'operazione passata nel messaggio SOAP. In
base al tipo di operazione verrà poi chiamato
il metodo che elaborerà il messaggio specifico.
Listato
5
public class ChatServlet extends JAXMServlet
implements
ReqRespListener {
public SOAPMessage onMessage(SOAPMessage
message) {
SOAPMessage msg = null;
try
{
SOAPPart sp = message.getSOAPPart();
SOAPEnvelope env
= sp.getEnvelope();
SOAPBody bdy = env.getBody();
//
Ottiene il contenuto di Body per determinare
// il messaggio
ricevuto
Iterator childs
= bdy.getChildElements();
SOAPElement e =
(SOAPElement)childs.next();
Name elementName
= e.getElementName();
String localName
= elementName.getLocalName();
// System.out.println(
"Chat Command: " + localName );
if(
localName.equals(LOGIN) ) {
msg
= login( env );
} else
if( localName.equals(LOGOUT) ) {
msg
= logout( env );
} else
if( localName.equals(SEND_MESSAGE) ) {
msg
= send( env );
} else
if( localName.equals(GET_MESSAGES) ) {
msg
= get( env );
}
}
catch(Exception e){
e.printStackTrace();
try {
msg
= Utils.getFaultMessage("ChatServlet",
e.getMessage()
);
}
catch( SOAPException
e1 ){
System.err.println(
e1) ;
}
}
return
msg;
}
}
Nel
listato 5 é presente un estratto della Servlet
che mostra il metodo onMessage(). Come si può
notare, la Servlet implementa anche l'interfaccia ReqRespListener
(Request/Response). Questo indica che ad ogni chiamata
SOAP, la Servlet risponde con un altro messaggio SOAP
a differenza della modalità Oneway che specifica
il solo invio del messaggio ma non una sua risposta.
Il codice di navigazione all'interno del messaggio SOAP
é similare a quello presente nella classe Utils
in quanto in entrambi i casi si utilizza l'object model
SOAP di JAXM.
Per completare la conformità della Servlet alle
specifiche SOAP, in caso di qualsiasi eccezione che
si dovesse presentare nel codice di gestione dell'operazione,
viene generato un messaggio Fault e ritornato come risposta.
Ovviamente il client, in questo caso, dovrà essere
pronto ad elaborare una risposta diversa da quella attesa
ed a comportarsi di conseguenza.
Le funzioni del server
Il metodo onMessage(), come detto, si occupa di capire
quale funzione é stata invocata e di reindirizzare
la chiamata al metodo opportuno. I metodi che implementano
le varie funzioni sono implementati all'interno della
Servlet stessa. Uno stralcio di questi é presente
nel listato 6.
Listato
6
public class ChatServlet extends JAXMServlet
implements
ReqRespListener {
protected SOAPMessage login(SOAPEnvelope
env )
throws
SOAPException {
boolean result = false;
HeaderData hd = examineHeader(
env );
if(
hd.isValid() )
result = register.add(
hd.user, new ChatBuffer() );
return
Utils.getConfirmMessage( LOGIN, result );
}
protected SOAPMessage send(SOAPEnvelope
env)
throws SOAPException{
boolean result = false;
HeaderData hd = examineHeader(
env );
if(
hd.isValid() ) {
String message =
null;
SOAPBody
bdy = env.getBody();
Iterator childs
= bdy.getChildElements();
while( childs.hasNext()
) {
SOAPElement
e = (SOAPElement)childs.next();
Iterator
ch = e.getChildElements();
Text
text = (Text)ch.next();
message
= text.getValue();
}
if(
message != null ) {
register.talk(
hd.user, message );
result
= true;
}
}
return
Utils.getConfirmMessage( SEND_MESSAGE, result );
}
protected
SOAPMessage get(SOAPEnvelope env)
throws
SOAPException {
SOAPMessage result = null;
HeaderData hd = examineHeader(
env );
if(
hd.isValid() ) {
result = Utils.getGetMessagesResponse(
register.getMessages(
hd.user));
} else {
result = Utils.getConfirmMessage(
GET_MESSAGES, false );
}
return
result;
}
}
A
parte i controlli legati all'header ed espletati tramite
la classe HeaderData, i metodi possono essere molto
semplici, come login(), o più complessi, come
send(). Nel primo caso il programma non fa altro che
restituire il messaggio di conferma SOAP definito nel
protocollo e generato tramite la classe Utils. Nel secondo
caso, l'operazione é più complessa in
quanto é necessario estrarre il parametro del
comando (il testo da inviare alla Chat) per poi solo
successivamente inviare il messaggio di risposta.
Alcune considerazioni
SOAP porta ad un approccio procedurale del codice. Nonostante
la O si SOAP stia per Object, sembra che l'accezione
di quest'ultimo sia più da intendere come oggetto
"alla Microsoft". Per MS, infatti, il concetto
di oggetto equivale a quello di componente, visto anche
l'impegno profuso ai tempi per vendere Visual Basic
come un linguaggio ad oggetti. Implementando dunque
il codice di accesso a SOAP, il risultato é abbastanza
procedurale (vedi la classe Utils). Certo, si sarebbe
potuto creare una implementazione di ChatProxy ad oggetti,
ma probabilmente con un certo sforzo che non mi sembra
avrebbe ripagato in termini di flessibilità e
manutenibilità.
Un altra osservazione é sul messaggio Fault.
In un certo senso le risposte SOAP non sono strong-typed
in quanto una risposta potrebbe essere quella prevista,
potrebbe essere leggermente diversa (ad esempio se cambia
leggermente l'implementazione del codice che fornisce
la risposta), oppure completamente diversa, come nel
caso di un Fault. In un certo senso é come se
un metodo ritornasse una stringa oppure un booleano.
Sicuramente la gestione degli errori basati su eccezioni
fornisce un modello più chiaro rispetto al cambio
radicale del messaggio.
Sicuramente a basso livello queste questioni non rivestono
un problema, in quanto sostanzialmente replicano le
prassi in uso per la comunicazione inter-processo. Trattare
però direttamente queste informazioni ad alto
livello é scomodo, sopratutto se in confronto
ad RMI. Probabilmente la soluzione la fornisce JAX-RPC
(si veda [7]) che costruisce su SOAP/JAXM un modello
più vicino al mondo Java.
Conclusioni
La chat cominicia a prendere forma. Sebbene il lato
server sia ora implementato, non é possibile
provarlo come la Chat originale, tramite telnet. O meglio:
é possibile ma molto scomodo perché il
protocollo non é testuale - e quindi digitabile
facilmente da un essere umano - ma prevede molte più
informazioni. Per provare finalmente l'applicazione
si dovrà attendere la stesura del client, cosa
che avverrà il mese prossimo nella terza ed ultima
puntata.
Bibliografia
[1] Michele Sciabarrà - "Java Relay Chat",
Mokabyte, febbraio 1998
[2] Michele Sciabarrà - "Java Relay Chat,
parte 2", Mokabyte, aprile 1998
[3] Michele Sciabarrà - "Java Relay Chat,
parte 3", Mokabyte, maggio 1998
[4] Andrea Giovannini - "SOAP e Java: Integrazione
di applicazioni Java con il nuovo protocollo SOAP",
Mokabyte, gennaio 2001
[5] Massimiliano Bigatti - "Web Services (6) -
Java API for XML Messaging (JAXM)", Mokabyte, Marzo
2002
[6] Massimiliano Bigatti - "Una chat basata su
SOAP - parte prima", Mokabyte, Maggio 2002
[7] Massimiliano Bigatti - "Web Services (7) -
Semplificare applicazioni distribuite SOAP con JAX-RPC",
Mokabyte, Aprile 2002
Massimiliano Bigatti è
SUN Certified Enterprise Architect for Java Platform,
Enterprise Edition Technology. Si occupa di architetture
applicative basate su Java ed Internet, technical writing
e musica rock.
|