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:
-
Leggere il messaggio in arrivo
-
Creare una risposta
- 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<vMsgs.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
|