MokaByte 64 - Giugno 2002 
Una Chat basata su SOAP
II parte
di
Massimiliano
Bigatti
La scorsa volta abbiamo affrontato il protocollo applicativo della chat e ne abbiamo costruito una implementazione con SOAP. Ora é il momento di costruire codice funzionante per la creazione e fruizione di questi messaggi.

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.

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