MokaByte 85 - Maggio 2004 
Jordino
Un agente elettronico della NUMA al servizio di tutti quanti!

di
Claudio Levantini
Chi mai può dimenticare le gesta eroiche ed un po' improbabili del mitico Dirck Pitt? Eppure seguendo uno schema letterario inossidabile e di sicuro successo, l'autore ed inventore di questo James Bond all'ennesima potenza, gli affianca una spalla a cui solo il destino ha negato i riflettori del protagonista: Jordino!
Ma tutto questo cosa c'entra con Java?

Il Jordino che andiamo a presentarvi è il tentativo di costruire un agente che risponde in modo automatico a determinati messaggi di posta. E' il tentativo puramente didattico di costruirsi un "Majordomo" utilizzando il linguaggio java e tutte le potenzialità che lo stesso offre.
Ho deciso di chiamare il tutto "Jordino" un po per l'affinità fonetica con majordomo ed anche perchè il personaggio inventato da Cussler che fa da spalla a Pitt, in fondo, mi è molto più simpatico di Pitt stesso.

 

Formulazione del problema
Cerchiamo di descrivere in modo naturale e semplice che cosa dovrebbe fare la nostra applicazione: Un agente dovrebbe stare costantemente in ascolto su una porta pop3. Appena arriva un messaggio che è in grado di elaborare (il messaggio deve avere nel subject la stringa "[JD Comando"), lo deve prendere e mettere in una zona di parcheggio. Un ulteriore agente aspetta che qualcosa arrivi in tale zona, quando ciò avviene prende il messaggio in arrivo, lo esamina, crea la risposta e la spedisce al mittente del messaggio stesso.

 

A cosa serve?
Jordino, è una di quelle classiche applicazioni che uso definire "aperte", cioè che non nascono con uno scopo specifico anche se hanno un preciso ambito tecnologico. Le applicazioni aperte si contrappongono a quelle "chiuse", che sin dal loro concepimento cercano di risolvere un problema specifico. Questa è una differenza un po' sottile, e per chiarirla è forse bene fornire un esempio non informatico di applicazioni "aperte" ed applicazioni chiuse. Lasciamo per un attimo il mondo etereo dei bit e diamo un po' di esempi di applicazioni aperte e chiuse del mondo reale.

 

Applicazioni chiuse
Una tipica applicazione chiusa del mondo reale è la lavatrice, che per quanto possa avere un sacco di programmi e lavare un sacco di indumenti diversi, alla fine non può far altro che lavare. Altro esempio di applicazione chiusa può essere un modulo prestampato per il disbrigo di qualche procedura burocratica. Ultimo esempio di applicazione chiusa sono i modellini di treni.
Sempre partendo dal mondo reale, vediamo ora alcune applicazioni aperte che in qualche modo hanno a che fare con le applicazioni chiuse che abbiamo appena incontrato.

 

Applicazioni aperte
Se una lavatrice "pulisce", un coltello "taglia", sembra che entrambi gli oggetti svolgano un compito preciso, ma in qualche modo il "tagliare del coltello" ha un ambito di contesti molto più ampio del "pulire di una lavatrice". Infatti il coltello può essere anche "arma", oppure strumento per dimostrare abilità (pensate ai fachiri oppure ai lanciatori). Un'altro strumento aperto in contrapposizione al modulo pre-stampato è senza dubbio il foglio bianco, con il quale si possono fare un sacco di cose, la maggior parte delle quali non sono state ancora scoperte! E per quanto riguarda i trenini? Beh qui l'esempio da contrappore di applicazione aperta è facilissimo da trovare, non voglio fare pubblicità ma se dico "mattoncini", tutti noi pensiamo alla medesima cosa e credo che si tratti di uno dei giochi più aperti che sia mai stato inventato.

 

Un concetto analogico
Concedetemi ancora qualche riga di digressione per aggiungere che il concetto di applicazione chiusa o aperta non è un concetto digitale (vero o falso) ma piuttosto un concetto analogico.
Termino la digressione con una domanda un po' provocatoria: secondo voi qual'è lo strumento tecnologico più aperto che sia mai stato inventato? Io sarei indeciso tra due, ma il "tecnologico" risolve il dilemma in maniera molto precisa.

 

Jordino come applicazione aperta
A questo punto è chiaro cosa intendo quando dico che Jordino è un'applicazione aperta. Quello che può fare è abbastanza preciso, riconoscere determinati messaggi di posta elettronica e creare delle risposte automatiche. Ma i suoi ambiti applicativi non sono subito ben definibili, che è un po' un modo come un altro per dire che molto di quello che si può fare con Jordino dipende dalla fantasia di chi lo usa.

 

Analisi del problema
Per prima cosa cerchiamo di identificare gli attori che entrano in gioco:

Gli attori
-
Il cane da guardia sulla porta POP3 (WatchDog)
- La zona di parcheggio (MessageQueue)
- Una specie di piccolo scrivano fiorentino che risponde a tutti i messaggi depositati nella zona di parcheggio.

Appare subito abbastanza chiaro che l'oggetto più oberato di lavoro è, per mantenere fede alla metafora letteraria, il piccolo scrivano fiorentino; egli deve svolgere tre compiti di non poco conto:

  1. Leggere il messaggio in arrivo
  2. Creare una risposta
  3. Inviare una risposta

Per alleviare un po' le fatiche del nostro ragazzetto, identifichiamo un ulteriore attore (il postino) che si incaricherà di svolgere il primo e terzo compito: ricevere e spedire una risposta (MailReader).
Allo scrivano non resta ora che leggere la posta che il postino gli recapita e rispondere in maniera consona (AnswerFactory).

La Recita
Movimentiamo un po le cose, cercando di nominare le azioni che sono peculiari ad ogni attore:

  • Il cane da guardia sulla porta POP3
    1 -
    Capire quando è arrivata nuova posta (metodo run della classe WatchDog)
    2 -
    Postare la posta in arrivo nell'area di parcheggio
    3 -
    Sonnecchiare per qualche tempo per poi riprendere con più grinta dal principio.
  • La zona di parcheggio
    1 - Aggiungere un messaggio (metodo push della classe MessageQueue)
    2 - Restituire un messaggio (metodo pop della classe MessageQueue)

  • Il piccolo scrivano fiorentino
    1 - Creare una risposta adatta in base ad un dato messaggio (metodo run della classe AnswerFactory)

  • Il postino
    1 - Prendere un messaggio dalla zona di parcheggio (metodo run della classe MailReader)
    2 - Darlo al piccolo scrivano fiorentino perchè ne crei una risposta
    3 - Spedire la risposta creata

Fatto tutto questo, magicamente la nostra struttura comincia a prendere vita e possiamo accorgerci di alcune caratteristiche:

  • Il cane da guardia ed il postino possono essere considerati come processi "quasi" indipendenti ed il quasi è dettato dal fatto che il postino non può spedire nulla se non vi è nulla da spedire.
  • La zona di parcheggio è quella che costituisce il trade-union tra questi due elementi.
  • Il postino dormicchia ed appena in zona di parcheggio arriva un messaggio su cui operare, scatta un'enorme sveglia. A questo punto il postino prende il messaggio, lo passa allo scrivano,e ricevuta la risposta, la spedisce, poi se ne torna tranquillamente a fare la nanna.
  • La struttura tecnico organizzativa di internet ci aiuta rispetto al nostro disegno in quanto i due canali di ricezione e spedizione sono indipendenti. Il cane da guardia ascolta sulla porta pop mentre il postino spedisce sulla porta smtp. Questo ci da la possibilità di considerare i due processi come processi diversi. Al limite potrebbero essere implementati anche su CPU diverse.
  • La sincronizzazione dei due processi avviene grazie alla zona di parcheggio.
  • Sempre per rimanere fedele alla sua metafora letteraria,il piccolo scrivano lavora nell'ombra ma il suo lavoro è tutt'altro che banale. In lui risiede tutta l'intelligenza applicativa. Deve interpretare i messaggi in arrivo e formulare delle risposte coerenti. In sostanza è la mente di tutta l'applicazione, più curate questo elemento e più sarete in grado di stupire i vostri utenti con effetti ultra-speciali.

 

Produttore e consumatore
Già nella sua descrizione naturale il dominio del problema suggerisce uno dei possibili percorsi per la sua soluzione. Da una parte abiamo il cane da guardia che non appena possibile produce un messaggio da elaborare. Dall'altra c'è il postino che non vede l'ora, ricevuto un messaggio di "consumarlo" consegnandolo al piccolo scrivano fiorentino.

Ed è proprio facendo riferimento al pattern Producer/Consumer che è stata disegnata tutta l'applicazione. In pratica il postino aspetta che il cane da guardia lo avvisi dell'arrivo di un messaggio. Dopo di che il postino si mette al lavoro ed il cane sonnecchia nell'attesa che lo zelante postino lo informi che ha consegnato il messaggio e si rimette in attesa di ulteriore lavoro.
Ora è il cane da guardia che ritorna operoso e se ci sono messaggi sveglia nuovamente il postino.

Fuor di metafora, il cane da guardia è una classe di tipo Thread (Watchdog) ed il piccolo scrivano fiorentino anche lui è un Thread (MailReader) che si sincronizzano entrambi su una coda di messaggi (MessageQueue).
Watchdog aspetta MailReader nel metodo push della coda di messaggi., in pratica è come dire che il cane da guardia prima di inserire un nuovo messaggio nella coda aspetta che il postino gli dica che ha recapitato l'ultimo messaggio.
In maniera funzionalmente simmetrica, la classe MailReader aspetta Watchdog nel metodo pop della coda dei messaggi. Detto naturalmente e come se il postino aspettasse l'informazione dal cane da guardia che un nuovo messaggio è pronto per essere spedito.
Un meccanismo del tutto analogo è spiegato nell'apposita sezione del Tutorial di Sun.
Prima di lanciarci in un'analisi più dettagliata del codice sorgente, diamo un'occhiata al class-diagram di Jordino.


Figura 1 - Class diagram della applicazione

Un'occhiata ai sorgenti

Cerchiamo ora di entrare un po' più nel vivo del codice descrivendo in linea generale la sua strutturazione, per focalizzare l'attenzione in maniera un po' più accurata su quelle parti non banali dell'implementazione. Il progettino è diviso in tre packages:

  • gui - Rappresenta l'interfaccia grafica;
  • core - Contiene gli oggetti principali del pacchetto;
  • answer - contiene tutte le risposte che vengono generate dal sistema.

Un codice main semplicissimo genera e visualizza l'interfaccia del programma.


public static void main(String[] argv){
  JordinoMainWin dWin=new JordinoMainWin();
  dWin.pack();
  dWin.show();
} // end main

La frame principale del programma (JordinoMainWin.java) contiene i tre oggeti del pacchetto core:

* WatchDoc e ReadMailer che derivano entrambi dalla classe Thread;
* MessageQueue che è il repository dei messaggi da elagorarre.


/* No graphic elements */
MessageQueue dQueue=new MessageQueue();
MailReader dReader;
WatchDog dWatcher;

Alla partenza dell'applicazione l'utente può premere il bottone "Start" che istanzia i due thread passandogli la coda di messaggi creata in precedenza. I due thread usano entrambi la stessa coda di messaggi come si trattasse di un oggetto sincronizzatore. Vedremo più avanti come funziona questo meccanismo di sincronia.


if(anEvent.getActionCommand().equals("Start")){
  dReader=new MailReader(dQueue);
  dReader.setSemaphore(true);
  dReader.start();
  dWatcher=new WatchDog(dQueue);
  dWatcher.setSemaphore(true);
  dWatcher.start();  
  dBtnTurn.setLabel("Stop");
}

Il cane da guardia
WatchDog compie tutto il suo lavoro, come è normale per un thread nel metodo run. Letti alcuni parametri di configurazione, viene aperta una connessione pop3.


System.out.println("WatchDog Run...");
dStatus=0;
Properties vProp=System.getProperties();
Session vSession = Session.getDefaultInstance(vProp,null);
if(dDebug)
  vSession.setDebug(true);
  try{
    // Store vStore = vSession.getStore("imap");
    Store vStore = vSession.getStore("pop3");
    vStore.connect(dPop,dUser,dPassword);
    Folder vFolder = vStore.getFolder(dFolderName);
    if(vFolder!=null && vFolder.exists()){
      // vFolder.addMessageCountListener(new Dog(dQueue));
      System.out.println("WatchDoc active on "+dPop);

Fino a quando l'utente non blocca l'elaborazione premendo il tasto "Stop", WatchDog cicla eseguendo 4 compiti fondamentali:

1. apre il folder inbox della connessione pop;

vFolder.open(Folder.READ_WRITE);

2. inserisce eventuali nuovi messaggi nella coda dei messaggi;


Message[] vMsgs=vFolder.getMessages();
for(int i=0; i&ltvMsgs.length; i++){
  dQueue.push(vMsgs[i]);
} // end for

3. aspetta un determinato lasso di tempo;


try{
  sleep(dSleepSecs*1000);
} // end try
catch(InterruptedException anEx){
} // end catch

4. chiude il folder inbox.

vFolder.close(true);

 

Il postino
Anche per la classe MailReader il metodo chiave è il metodo run(). Per prima cosa si stabilisce una connessione con il canale smtp per la spedizione dei messaggi.


System.out.println("MailReader Run...");
Properties vProp= System.getProperties();
vProp.put("mail.smtp.host",dSmtp);
Session vSession= Session.getDefaultInstance(vProp, null);
System.out.println("MailReader started.");

Anche in questo metodo è presente un ciclo while che può essere interrotto quando l'utente preme il tasto "Stop" dell'interfaccia grafica. All'interno del ciclo viene preso un messaggio dalla coda e viene passato al piccolo scrivano per la creazione della risposta. La risposta viene poi spedita.


Message vMsg=dFactory.run(dQueue.pop());
if(vMsg!=null)
try{
  vMsg.setSentDate(new Date());
  Transport.send(vMsg);
}
// end try
catch(MessagingException anEx2){
  System.err.println(anEx2);
} // end catch

 

Sincronizzare due thread: la coda dei messaggi
Per riuscire a sincronizzare due thread, è necessario che questi due thread abbiano un "luogo comune" di lavoro. Nel nostro caso il luogo comune e la coda dei messaggi. WatchDog usa la coda per inserire messaggi, MailReader la usa per estrarli.

Risulta abbastanza chiaro a questo punto che i metodi fondamentali dove si gioca questo meccanismo sono il metodo pop() ed il metodo push() della classe MessageQueue. Non a caso questi due metodi vengono richiamati nel metodo run() dai due thread che abbiamo appena analizzato.

Per prima cosa vediamo il metodo pop(), dove il postino (Mailreader) aspetta che ci siano messaggi da passare al piccolo scrivano (AnswerFactory).


synchronized public MimeMessage pop(){
  boolean vForceExit=false;
  MimeMessage vRc=null;
  while(!vForceExit && dBuffer.isEmpty()){
    try{
      System.out.println("Sto aspettando nel pop()");
      wait();
    }
    catch(InterruptedException anEx){
      vForceExit=true;
    } // end catch
  } // end while
  notifyAll();
  if(!dBuffer.isEmpty())
    vRc=(MimeMessage)dBuffer.removeLast();
  return vRc;
} // end pop

Il ciclo while che incontriamo immediatamente ha lo scopo di far aspettare (istruzione wait()) l'oggetto MailReader fino a quando non ci sono messaggi da elaborare. Le condizioni di uscita sono due:

  • ci sono messaggi da elaborare;
  • l'utente ha forzato il termine del thread (istruzione intterrupt() nella classe JordinoMainWin).

Subito fuori dal ciclo, prima di restituire il messaggio che ha questo punto è presente nella coda, con l'istruzione notifyall() vengono svegliati tutti i thread in wait su questo oggetto (nel nostro caso l'oggetto WatchDoc). In pratica qui il postino è come se svegliasse il cane da guardia dicendogli che può tornare a recuperare nuovi messaggi dalla porta pop3 perchè lui si accinge a consegnare un messaggio allo scrivano.

Vediamo ora il metodo push() della stessa classe dove ci attenderemo che il cane da guardia aspetti che la coda sia vuota per poter inserire un nuovo messaggio recuperato dalla porta pop3.


synchronized public void push(Message aMsg){
  // Flag per l'interruzione del programma da parte dell'utente.
  boolean vForceExit=false;
  while(!vForceExit && !dBuffer.isEmpty()){
    // Entra nel ciclo se ci sono messaggi da processare.
  
  // Aspetta che vengano processati.
    try{
      System.out.println("Sto aspettando nel push()");
      wait();
    } // end try
    catch(InterruptedException anEx){
      vForceExit=true;
    } // end catch
   } // end while
   try {
     // Se tutti i messaggi nel buffer sono stati processati,
     // aggiunge un nuovo messaggio nel buffer e sveglia l'altro thread.
     if(aMsg.getSubject().indexOf("[JD")!=-1){
       dBuffer.addFirst(aMsg);
       aMsg.setFlag(Flags.Flag.DELETED,true);
       notifyAll();
      } // end if
    } // end try
    catch(MessagingException anEx){
      System.err.println(anEx);
    } // end catch
} // end push

Anche in questo metodo incontriamo immediatamente un ciclo while dove il thread aspetta le condizioni d'uscita che corrispondono al fatto che l'utente abbia forzato l'interruzione del programma oppure che la coda dei messaggi sia vuota.
Appena fuori dal ciclo, viene controllato il subject del messaggio, se è un messaggio di comando, viene aggiunto alla coda e cancellato dal server di posta.
Anche qui incontriamo la notifyall che sveglia tutti i thread in attesa sull'oggetto. Nel nostro caso questa è la sveglia per l'oggetto MailReader che sta aspettando di poter recuperare un messaggio e passarlo all'oggetto AnswerFactory.
Il piccolo scrivano fiorentino
Come già sottolineato questo è il vero cuoredell'applicazione. E' qui che stà tutta l'apertura di Jordino di cui abbiamo parlato in precedenza. Sono stati sviluppati in questa parte che corrisponde al package answer solo alcuni semplici messaggi di risposta, ma se volete, in questa sezione potete sbizzarrirvi e costruire le risposte automatiche più complesse. Ecco alcune delle idee che possono essere sviluppate:

  • gestione di una mailing list;
  • gestione di servizi on demand, come ad esempio notiziari costruiti su misura;
  • Servizi di pubblicazione sul web via posta elettronica;

Tutte le risposte devono derivare dalla classe Response. E' anche presente la classe AttachResp per la gestione di risposte che prevedano file allegati.
La classe AnswerFactoryè quella che, dato un messaggio in arrivo istanzia la corretta risposta. La generazione è statica ma con un'appropriata riscrittura del metodo run è possibile gestire una creazione dinamica delle classi.

 

Conclusione
Abbiamo analizzato un problema applicativo abbastanza interessante, suggerendo un disegno per la sua soluzione. Per il dominio del problema, java risulta essere un liguaggio molto adeguato per la realizzazione tecnica della soluzione disegnata qui sopra. I sorgenti allegati costituiscono questa situazione reale, e rimandiamo alla loro documentazioneper una descrizione specifica delle soluzioni tecniche adottate: thread, sincronizzazione ed altro. I sorgenti allegati sono perfettamente funzionanti ancorchè debbano essere considerati di carattere esclusivamente dimostrativo per due ordini di ragioni:

  • le casistiche applicative non sono esaustive;i test applicativi effettuati non sono stati esaustivi. A tal proposito segnaliamo che i test sono stati effettuati utilizzando come server di posta James del progetto jakarta.

 

Risorse
Scarica qui il file con l'esempio completo

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