MokaByte 65 - Luglio Agosto 2002 
Una Chat basata su SOAP
III parte
di
Massimiliano Bigatti
Una volta costruita la Servlet JAXM che fungerà da server della chat, é possibile passare alla codifica del client che, riutilizzando una parte di codice già scritto, si occuperà di esporre all'utente l'applicazione

Introduzione
La codifica del client suddivide la sua complessità in due aree distinte: l'interfaccia utente e la parte di protocollo SOAP. Per quanto riguarda la prima problematica, essendo questa un'applicazione Java2, l'implementazione sarà una mera applicazione delle API di Swing. Per quanto attiene invece alle problematiche di protocollo, sarà necessario ancora una volta fare uso delle JAXM, anche se mediate dalla classe di utilità Utils sviluppata nella seconda parte di questa miniserie.


Il client
Il codice client vero e proprio é racchiuso all'interno della classe ChatClient (listato 1). Questa classe rappresenta dunque il controller lato client che trasforma le richieste applicative in opportuni messaggi SOAP. Per fare questo si appoggia alla classe Utils realizzata il mese scorso sfruttando i metodi di generazione dei messaggi e di invio. In particolare i metodi implementati sono quelli applicativi della Chat:

  • login();
  • logout();
  • send();
  • get();

Per eseguire l'invio del messaggio, il codice utilizza un oggetto SOAPConnection, sul quale chiama il metodo call(). Come noto, la chiamata va fatta ad un Endpoint che nel nostro caso é un oggetto di classe URLEndpoint fornito all'oggetto ChatClient dall'esterno tramite opportuno metodo "setter".
In applicazioni standalone (non contenute all'interno di Web Container) é necessario ottenere l'oggetto SOAPConnection dalla factory SOAPConnectionFactory (vedi Listato 2).

Listato 1
//...
public class ChatClient {

//...

protected void login() throws SOAPException {
  SOAPMessage msg = Utils.getLoginMessage( user, token );
  SOAPMessage reply = Utils.send( msg, serverEndpoint );
}

protected void logout() throws SOAPException {
  SOAPMessage msg = Utils.getLogoutMessage( user, token );
  SOAPMessage reply = Utils.send( msg, serverEndpoint );
}

protected void send( String message ) throws SOAPException {
  SOAPMessage msg = Utils.getSendMessage( user, token, message );
  SOAPMessage reply = Utils.send( msg, serverEndpoint );
}

protected String[] get() throws SOAPException {
  SOAPMessage msg = Utils.getGetMessage( user, token );
  SOAPMessage reply = Utils.send( msg, serverEndpoint );

  if( reply != null )
    return Utils.extractMessages( reply );
  else
    return null;
  }
}

Listato 2
//...
public class Utils {

//...

public static SOAPConnection getSOAPConnection()
                             throws SOAPException {
  
if( scf == null )
    
scf = SOAPConnectionFactory.newInstance();
  return scf.createConnection();
}

public static SOAPMessage send(SOAPMessage msg, Endpoint ep )
                               throws SOAPException {
  return getSOAPConnection().call( msg, ep );
}

//...

}


Sebbene ChatClient contenga un metodo main(), non é questa la classe da lanciare per eseguire la Chat. Da qui infatti non é possibile accedere all'interfaccia utente Swing in quanto il metodo main() contiene solo del codice di test per verificare il funzionamento del solo layer di comunicazione. Questo ci da però modo di provare il server, almeno dalla console.
Sempre nel metodo main() é possibile vedere il passaggio dell'endpoint alla classe ChatClient: in particolare nell'esempio é presente la stringa "http://127.0.0.1:8080/soapchat/chat". Questo significa che la web application é stata chiamata "soapchat" e che il tutto é in test sulla macchina locale.
Questo parametro dovrà riflettere l'installazione specifica che sarà adottata, solo nel caso si intenda provare questa parte di test a basso livello.


L'interfaccia utente
Per supportare le due diverse tipologie di client, applicazione ed applet, il codice Swing deve essere realizzato in modo svincolato da queste due entità. ChatPanel estende dunque JPanel: questo componente visuale potrà poi essere utilizzato, sia all'interno di una Applet, sia in una applicazione standalone basata, ad esempio, su JFrame.
La stessa classe ChatPanel, comunque, contiene anche l'implementazione dell'applicazione standalone, tramite il metodo main() (listato 3).

Listato 3
//...
public class ChatPanel extends JPanel {
  //...
  public static void main(String[] args) {
    JFrame f = new JFrame( "Chat Client - utente: " + args[0] );
    f.getContentPane().add(new ChatPanel(args[0],
                                         "max/127.0.0.1"));
    f.pack();
    f.setVisible( true );
  }
}



Come si evince dalla chiamata al costruttore di JFrame, il metodo main() si aspetta almeno un parametro che rappresenta il nome dell'utente. Questo é il nickname che verrà utilizzato all'interno dell'applicazione.
La costruzione dell'interfaccia utente é banale e prevede un'area di testo per i messaggi, un campo di testo per il messaggio da inviare e due pulsanti: invia ed esci. L'intera operazione di creazione della UI avviene nel metodo createUI(). Per i componenti visuali che dovranno essere oggetto di modifiche successive, verrà tenuto un riferimento in opportuni attributi di classe. Ad esempio l'area messaggi, che dovrà essere riempita con i messaggi scaricati dal server, oppure il campo di immissione, che dovrà fornire il testo da inviare agli altri utenti connessi, dovranno avere un riferimento accessibile dagli altri metodi della classe (listato 4).

Listato 4
//...
public class ChatPanel extends JPanel {

//...

protected void createUI() {
  messages = new JTextArea(25,40);
  messages.setEditable( false );

  input = new JTextField(40);
  submit = new JButton( "Invia" );
  submit.addActionListener( new ActionListener() {
    public void actionPerformed( ActionEvent e ) {
    send();}});

  fine = new JButton( "Fine" );
  fine.addActionListener( new ActionListener() {
    public void actionPerformed( ActionEvent e ) {
    logout();}});

  JPanel right = new JPanel( new FlowLayout() );
  right.add( submit );
  right.add( fine );

  JPanel left = new JPanel( new FlowLayout() );
  left.add( input );

  JPanel bottom = new JPanel( new BorderLayout() );
  bottom.add( left, BorderLayout.WEST );
  bottom.add( right, BorderLayout.EAST );

  setLayout( new BorderLayout() );
  add( new JScrollPane(messages), BorderLayout.CENTER );
  add( bottom, BorderLayout.SOUTH );
  }
}


Il costruttore della classe si occuperà dunque di creare l'interfaccia utente, ma anche di inizializzare l'oggetto ChatClient per la comunicazione con il server di chat ed anche il thread client che si occuperà di rinfrescare la finestra dei messaggi.
Nel listato 5 é presente la porzione di codice relativa. Si può notare come, nel caso la connessione al server non avvenga con successo, l'errore venga presentato all'interno della finestra dei messaggi. Inoltre si noti la presenza dell'endpoint codificato all'interno del codice. Ovviamente una soluzione più corretta é quella di esternalizzare questa informazione, magari tramite un parametro di un file di proprietà.

Listato 5
//...
public class ChatPanel extends JPanel {

//...

public ChatPanel( String user, String token ) {
super();

this.user = user;
this.token = token;

createUI();

try {
  cc = new ChatClient( user, token );
  cc.setEndpoint( "http://127.0.0.1:8080/soapchat/chat" );
  cc.login();
}
catch( SOAPException ex ) {
  addMessage( "Impossibile connettersi al server della chat" );
  addMessage( ex.toString() );
  input.setEnabled( false );
}

  readingThread = new Thread( new Runnable() {
    public void run() {
      while( doReading ) {
        getMessages();
        try {
          Thread.sleep(1000);
        } catch( InterruptedException e ) {
        System.err.println( e );
        }
      }
    }
  });

  readingThread.start();
  }
}

Il thread readingThread, come detto, si occupa di leggere ogni secondo la lista dei messaggi ancora da leggere. La frequenza di lettura può essere ovviamente modificata e comporterà un maggior carico al server, minore sarà il tempo di attesa del thread. Questo esegue il ciclo fino a che non riceve ordine di concludere, evento rilevato interrogando la variabile doReading.


Implementare l'operatività
Una volta costruita l'involucro entro cui avverranno le operazioni dell'utente, é necessario collegare l'interfaccia utente alle chiamate SOAP implementate da ChatClient.
Nel listato 6 é possibile vedere il metodo getMessages(). Questo viene richiamato dal thread per rinfrescare la finestra dei messaggi ricevuti dall'utente. Il codice non fa altro che invocare l'oggetto ChatClient, recuperare l'array dei messaggi ricevuti e metterli a video tramite il metodo addMessage().

Listato 6
protected void getMessages() {
  try {
    String[] m = cc.get();
    for( int i=0; i<m.length; i++ )
      addMessage( m[i] );
  }
  catch( SOAPException ex ) {
    addMessage( "Impossibile leggere i messaggi dal server" );
    addMessage( ex.toString() );
  }
}


Quando l'utente desidera inviare un messaggio, digita il testo nel campo relativo e fa clic sul pulsante di invio. Questo invoca il metodo send() che a sua volta estrae il testo digitato e lo passa all'omonimo metodo di ChatClient. Gli eventuali errori sono come di consueto inviati nella finestra dei messaggi. (Listato 7)

Listato 7
protected void send() {
  try {
    cc.send( input.getText() );
  } catch( SOAPException ex ) {
    addMessage( "Impossibile inviare il messaggio" );
    addMessage( ex.toString() );
  }
  getMessages();
}

Quando l'utente desidera concludere la sessione di Chat, fa clic sul pulsante di disconnessione che richiama il metodo logout(). Questo, oltre a chiamare logout() su ChatClient, pone doReading a false ed esce dalla Virtual Machine.

Listato 8
protected void logout() {
try {
cc.logout();
} catch( SOAPException ex ) {
System.err.println("Errore in fase di disconnessione dal server di chat");
}
doReading = false;
System.exit(0);
}


Il client standalone è visibile in Figura 1.


Figura 1



L'Applet
Implementando l'applet client (listato 9), è stato riscontrato un problema con l'implementazione del runtime di JAXM.

Listato 9
import java.applet.*;
import javax.swing.*;

public class ChatApplet extends JApplet {

  ChatPanel cp;

  public void init() {
  
  String user = getParameter("user");
    String passwd = getParameter("passwd");

    user = (user == null) ? "guest" : user;
    passwd = (passwd == null ) ? "guest" : passwd;

    cp = new ChatPanel( user, user+passwd);
    getContentPane().add( cp );
    cp.suspend();
  }

  public void start() {
    cp.resume();
  }

  public void stop() {
    cp.suspend();
  }
}


Il codice che si occupa di eseguire la POST del messaggio SOAP, fa uso del metodo setRequestMethod() sulla classe java.net.HttpURLConnection, il cui utilizzo è vietato alle Applet. Il risultato è che il codice solleva un'eccezione di sicurezza se eseguito nella sand-box.
Nota: questa informazione è stata rilevata decompilando i sorgenti di jaxm-api.jar e dunque potrebbe non essere più vera in future release del software.
Per poter utilizzare JAXM, dunque, è stato necessario firmare l'applet. Una guida che spiega come fare è disponibile sul sito Javasoft al link [8], mentre altre informazioni sono disponibili sui forum relativi ([9]). In particolare sono stati utilizzati i seguenti comandi:

keytool -genkey -alias signFiles -keystore jaxmstore -keypass cic1995 -dname "cn=max" -storepass spcic95

jarsigner -keystore jaxmstore -storepass spcic95 -keypass cic1995 -signedjar ssp_client.jar sp_client.jar signFiles

keytool -export -keystore jaxmstore -storepass spcic95 -alias signFiles -file CompanyCer.cer

Per importare il certificato sul client, sono stati modificati i file di sistema cacerts e java.policy presenti sotto jre/lib/security.
In particolare, per importare un certificato è possibile utilizzare il seguente comando:

keytool -import -alias company -file CompanyCer.cer -keystore clientstore -storepass changeit

In figura 2 è possibile vedere l'esecuzione di due copie dell'applet, utilizzando i file client.html e client1.html vengono infatti inizializzate due copie della stessa Applet, passando nomi utente diversi.


Figura 2




Conclusioni
In questa miniserie composta da tre articoli abbiamo affrontato la problematica di costruire una semplice applicazione client/server basata su SOAP. Nella realizzazione del progetto sono state utilizzate le JAXM. Queste API di SUN, sebbene ancora giovani, hanno consentito di implementare con successo l'applicazione, sebbene sia servita una certa quantità di codice. Questo fatto é anche da imputare alla complessità: non é stato interfacciato un solo messaggio SOAP ma quattro diverse tipologie, più altre quattro per le rispettive risposte. Quando il livello di complessità sale, e il dominio applicativo si sposta da semplici problematiche di messaging a vere e proprie applicazioni distribuite (sebbene semplici), può essere il caso di rivolgersi ad altri strumenti, come le JAX-RPC che consentono un maggior livello di astrazione rispetto al messaggio nudo e crudo.
Un altro problema è il meccanismo di request/response utilizzato nell'applicazione: al crescere del numero di client, le richieste al web server possono aumentare notevolmente e sottoporre questo componente ad un carico consistente. In questo caso sarebbe necessario utilizzare un approccio di tipo "notify", dove il server invia gli aggiornamenti ai diversi client, solo se questi sono effettivamente presenti. Purtroppo questa modalità non è disponibile nè in JAXM, nè in JAX-RPC ed inoltre non sarebbe attuabile utilizzando il protocollo HTTP che è di sua natura di tipo request/response. Per implementare un meccanismo di tipo "notify", sarebbe necessario utilizzare qualche tecnologia di push che però reintrodurrebbe quei problemi che risolti invece da HTTP.


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 - "Una chat basata su SOAP - parte seconda", Mokabyte, Giugno 2002
[8] Advanced Programming for the Java2 Platform. Chapter 10: Signed Applets http://developer.java.sun.com/developer/onlineTraining/Programming/JDCBook/signed.html
[9] http://forum.java.sun.com/thread.jsp?forum=63&thread=174214


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 di ... www.javawebservices.it.

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