SAPIENZA – Università di Roma Facoltà di Ingegneria Corso di Laurea in Ingegneria Gestionale e Ingegneria Informatica Dispensa didattica JBoss Application Server e Sviluppo di Applicazioni Distribuite in J2EE. Concetti di Base ed Esempi. M. Bartiromo, V. De Angelis, M. de Leoni, F. de Leoni, M. Mecella, R. Russo, S. Saltarelli Creative Commons License Deed Attribuzione - Non commerciale - Non opere derivate 2.5 Italia Tu sei libero: • di riprodurre, distribuire, comunicare al pubblico, esporre in pubblico, rappresentare, eseguire e recitare questa opera Alle seguenti condizioni: Attribuzione. Devi attribuire la paternità dell’opera nei modi indicati dall’autore o da chi ti ha dato l’opera in licenza. Non commerciale. Non puoi usare questa opera per fini commerciali. Non opere derivate. Non puoi alterare o trasformare quest’opera, nè usarla per crearne un’altra. Ogni volta che usi o distribuisci questa opera, devi farlo secondo i termini di questa licenza, che va comunicata con chiarezza. In ogni caso, puoi concordare col titolare dei diritti d’autore utilizzi di quest’opera non consentiti da questa licenza. Niente in questa licenza indebolisce o restringe i diritti degli autori. Le utilizzazioni consentite dalla legge sul diritto d’autore e gli altri diritti non sono in alcun modo limitati da quanto sopra. Questo è un riassunto in linguaggio accessibile a tutti del Codice Legale (la licenza integrale) disponibile all’indirizzo: http://creativecommons.org/licenses/by-nc-nd/2.5/it/legalcode. Indice Introduzione 5 1 RMI 1.1 Dalle RPC a RMI (Object Broker) . . . . . . . 1.2 La serializzazione e la trasmissione degli oggetti 1.3 Architettura RMI . . . . . . . . . . . . . . . . . 1.4 Uso di RMI . . . . . . . . . . . . . . . . . . . . 1.5 Esempio: Sender e Receiver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Swing 2.1 Il package Swing . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Top Level Container . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Uso di JFrame . . . . . . . . . . . . . . . . . . . . . . . 2.3 Paranoramica di alcuni widget . . . . . . . . . . . . . . . . . . 2.4 L’ereditarietà per personalizzare i frame . . . . . . . . . . . . 2.5 I Layout Manager e la gerarchia di contenimento . . . . . . . . 2.5.1 Layout Management . . . . . . . . . . . . . . . . . . . 2.5.2 Progettazione della GUI con le gerarchie di contenimento 2.5.3 Progettazione top down di interfacce grafiche . . . . . . 2.6 La Gestione degli Eventi . . . . . . . . . . . . . . . . . . . . . 2.6.1 Implementazione dell’event delegation . . . . . . . . . . 2.6.2 Un esempio: elaborare gli eventi del mouse . . . . . . . 2.6.3 Uso di adapter nella definizione degli ascoltatori . . . . 2.7 La gestione degli eventi Azione . . . . . . . . . . . . . . . . . 2.8 Accedere dall’ascoltatore agli oggetti di una finestra . . . . . . 2.9 Condividere gli ascoltatori per più oggetti . . . . . . . . . . . 2.10 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 10 13 16 25 30 31 32 33 34 39 41 41 45 47 51 51 53 55 55 57 61 67 3 Servlet e Java Server Pages (JSP) 68 3.1 Introduzione alle Servlet . . . . . . . . . . . . . . . . . . . . . 68 3.2 Il Ciclo di Vita di una Servlet . . . . . . . . . . . . . . . . . . 69 2 INDICE 3.3 Come Interagire con una Servlet: Richieste 3.3.1 Esempio Data . . . . . . . . . . . . 3.3.2 Esempio Rubrica . . . . . . . . . . 3.4 Le Sessioni . . . . . . . . . . . . . . . . . . 3.4.1 Esempio Login . . . . . . . . . . . 3.5 Le Java Server Pages (JSP) . . . . . . . . 3.5.1 Esempio Lista Prodotti (carrello) . 3 e . . . . . . Risposte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 72 77 82 85 88 94 4 JMS: Enterprise Messaging (ed introduzione su MOM) 102 4.1 Introduzione al MOM (richiami) . . . . . . . . . . . . . . . . . 103 4.1.1 Modelli di Messaging . . . . . . . . . . . . . . . . . . . 103 4.2 Introduzione a JMS . . . . . . . . . . . . . . . . . . . . . . . . 105 5 Enterprise Java Bean (EJB) 114 5.1 Introduzione agli EJB . . . . . . . . . . . . . . . . . . . . . . 114 5.2 Fondamenti degli EJB . . . . . . . . . . . . . . . . . . . . . . 116 5.3 Scheletro di un Enterprise Bean . . . . . . . . . . . . . . . . . 119 5.3.1 Enterprise Bean Class . . . . . . . . . . . . . . . . . . 119 5.3.2 L’EJB Object . . . . . . . . . . . . . . . . . . . . . . . 120 5.3.3 L’Home Object . . . . . . . . . . . . . . . . . . . . . . 121 5.3.4 Le interfacce locali . . . . . . . . . . . . . . . . . . . . 122 5.3.5 Deployment Descriptor . . . . . . . . . . . . . . . . . . 123 5.4 Session Bean . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 5.4.1 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 5.4.2 Ciclo vita dei Session Bean . . . . . . . . . . . . . . . . 127 5.4.3 Creazione di un Session Bean Stateless . . . . . . . . . 129 5.4.4 Creazione di un Session Bean Stateful . . . . . . . . . . 136 5.5 Entity Bean . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 5.5.1 Caratteristiche . . . . . . . . . . . . . . . . . . . . . . 145 5.5.2 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 5.5.3 Creazione di un Entity Bean Bean-Managed persistent 148 5.5.4 Creazione di un Entity Bean Container-Managed persistent . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 5.6 Message-driven Bean . . . . . . . . . . . . . . . . . . . . . . . 184 5.6.1 Utilizzo dei MDB . . . . . . . . . . . . . . . . . . . . . 184 6 Applicazione completa sugli EJB 6.1 Vendita di libri on-line . . . . . . . . . . . 6.2 Alcuni Diagrammi UML dell’Applicazione 6.3 Package BookOrder . . . . . . . . . . . . . 6.4 Package BookOrderMDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 186 191 193 194 INDICE 6.5 6.6 6.7 6.8 6.9 Package Archivio . . . . Package BookStoreSwing Modulo Accesso.war . . Il deployment descriptor Provare l’applicazione . . 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 204 204 215 215 7 Web Service 229 7.1 Le tecnologie alla base dei Web Services . . . . . . . . . . . . 230 7.1.1 SOAP: Simple Object Access Protocol . . . . . . . . . 232 7.1.2 WSDL: Web Service Definition Language . . . . . . . . 235 7.1.3 UDDI: Universal Description Discovery and Integration 244 8 Applicazione completa sui Web Service 248 8.1 Il Web Service . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 8.2 Il Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 Appendice 257 A.1 Servlet e JSP . . . . . . . . . . . . . . . . . . . . . . . 257 A.2 Uso di JMS con JBOSS . . . . . . . . . . . . . . . . . 258 Bibliografia 261 Introduzione Oggigiorno, un numero sempre crescente di sviluppatori di sistemi software per le imprese sta spostando il suo interesse verso applicazioni distribuite transazionali con lo scopo di usufruire della maggiore velocità, sicurezza e affidabilità delle tecnologie server-side. Molti, infatti, sono i vantaggi derivanti dall’adozione di un modello distribuito: un sensibile miglioramento delle prestazioni complessive, una maggiore semplicità nella gestione delle risorse distribuite e un sostanziale incremento delle potenzialità operative. Ad esempio, si può pensare di suddividere un processo computazionalmente pesante in sottoroutine più piccole ed eseguire tali “pezzi di applicazione” su macchine diverse, ottenendo una drastica diminuzione del tempo complessivo di esecuzione. L’esigente mondo dell’e-commerce e dell’information technology, in continuo cambiamento, richiede che le applicazioni aziendali siano progettate e costruite con uso di minori risorse economiche e maggiore velocità di quanto non accadesse in precedenza. Un importante scenario in cui è utile l’utilizzo del modello distribuito è quello in cui si devono realizzare applicazioni che si interfacciano con codice legacy: in tal caso si può pensare di inglobare gli applicativi esistenti (legacy appunto) in oggetti remoti e pilotarne in tal modo le funzionalità da un client scritto con i nuovi paradigmi di programmazione. Per programmazione distribuita si intende tipicamente quel particolare paradigma computazionale in cui una applicazione viene organizzata in moduli differenti, localizzati in spazi di indirizzamento diversi tra loro. La programmazione distribuita consente di realizzare sistemi service-oriented in cui un componente A è in grado di invocare i servizi di un componente B dispiegato su un computer remoto: in tale scenario, per semplicità, si definisce client il componente chiamante, mentre il remoto è detto server. I sistemi distribuiti sono generalmente costruiti al di sopra di un middleware. Esso fornisce astrazioni di programmazione che nascondono alcune delle complessità che costituiscono la realizzazione di un’applicazione distribuita. I diversi componenti che cooperano a fornire un servizio composito 5 Introduzione possono essere dispiegati su piattaforme diverse. I middleware hanno quindi anche il compito di nascondere l’eterogeneità delle piattaforme dietro una astrazione comune. Il crescente uso del Web come canale di accesso ai sistemi informativi ha forzato le piattaforme middleware a fornire il supporto per l’accesso al Web. Questo supporto è tipicamente fornito sotto forma di Application Server. Gli Application Server sono equivalenti alle piattaforme middleware. La differenza principale è l’incorporazione del Web come canale di accesso ai servizi implementati utilizzando il middleware. Le tecnologie su cui possono basarsi gli Application Server sono due: Microsoft .NET e Java di Sun. In termini di funzionalità, gli Application Server basati su Java e quelli basati su .NET presentano molte similitudini. Ciononostante, quella basata su Java è nettamente più diffusa e accettata, ed annovera numerose implementazioni sia commerciali che open source. Per ridurre i costi, il progetto e lo sviluppo di applicazioni, la Piattaforma Java2 Enterprise Edition (J2EE) fornisce un approccio basato sui componenti per la progettazione, lo sviluppo, l’assemblamento e il dispiegamento delle applicazioni d’impresa. La piattaforma J2EE offre un modello di applicazione distribuita multilivello, componenti riusabili, un modello di sicurezza unificato, un controllo flessibile delle transazioni, e un supporto ai servizi web mediante scambio di informazioni basato su XML. Generalmente, gli Application Server di tipo open source vengono impiegati per la ricerca, la didattica e nelle piccole e medie imprese, che non possono permettersi soluzioni commerciali. Spesso, questi prodotti “a basso costo” non hanno nulla da invidiare alle soluzioni commerciali, sia nell’efficienza che nelle fnzionalità disponibili. Tuttavia, soffrono di una scarsa documentazione. Alla luce di queste considerazioni, questo lavoro ha come obbiettivo quello di fornire una valida documentazione di supporto all’uso delle tecnologie J2EE, con particolare attenzione a uno dei maggiori e più conosciuti Application Server di tipo open source: JBOSS. In particolare, questo materiale è utilizzato come supporto didattico dagli studenti del corso di Progettazione del Software 2, tenuto dall’Ing. Massimo Mecella nei corsi di Laurea Specialistica in Ingegneria Informatica e in Ingegneria Gestionale di SAPIENZA – Università di Roma, per l’anno accademico 2006/2007 e 2007/2008. 6 Capitolo 1 RMI 1.1 Dalle RPC a RMI (Object Broker) Uno dei requisiti fondamentali per implementare un sistema distribuito è disporre di un sistema di comunicazione fra macchine diverse, basato su standard e protocolli prestabiliti. In Java la gestione dei socket è un compito relativamente semplice, tanto che si possono realizzare in maniera veloce sistemi di comunicazione con i quali scambiare informazioni in rete o controllare sistemi remoti. L’implementazione di una connessione via socket risolve però solo il problema di fondo (come instaurare la connessione) ma lascia in sospeso tutta la parte di definizione delle modalità di invocazione e dei vari protocolli per lo scambio delle informazioni. Prima dell’introduzione di RMI, erano già disponibili strumenti per l’esecuzione di codice remoto, basti pensare alla Remote Procedure Call (RPC). Con questa tecnologia è possibile gestire procedure facenti parte di applicazioni remote rispetto al chiamante. Le RPC furono introdotte all’inizio degli anni Ottanta e divennero immediatamente la base per costruire sistemi 2-tier, i quali ereditarono molte delle notazioni e delle assunzioni usate a partire dalle RPC. In particolare, le RPC stabilirono la nozione di client (il programma che chiama una procedura remota) e di server (il programma che implementa la chiamata di procedura remota che viene invocata), nonchè il concetto di Interface Definition Language (IDL). La forza delle RPC era il fatto che esse si basavano su un concetto che i programmatori di quel tempo conoscevano molto bene: la procedura. Le RPC resero possibile cominciare a costruire applicazioni distribuite senza dover cambiare il linguaggio o il paradigma di programmazione. Molti sono i vantaggi derivanti dall’adozione di tale modello: un sensibile miglioramento delle prestazioni complessive, una maggiore semplicità nella gestione delle risorse distribuite, e un sostanziale incremento delle 7 1.1 Dalle RPC a RMI (Object Broker) potenzialità operative. Ad esempio si può pensare di suddividere un processo computazionalmente pesante in subroutine più piccole ed eseguire tali “pezzi di applicazione” su macchine diverse ottenendo una drastica diminuzione del tempo complessivo di esecuzione. Nel caso in cui invece l’efficienza non sia l’obiettivo principale, si può comunque trarre vantaggio da una organizzazione distribuita, potendo gestire meglio e più semplicemente le varie risorse localizzate nei differenti spazi di indirizzamento. Si pensi per esempio a una strutturazione a tre livelli (3-Tier) per la gestione di database relazionali in Internet: dal punto di vista del client ci si deve preoccupare esclusivamente dell’interfacciamento con l’utente e dello scambio con il server remoto delle informazioni contenute nel database. Un altro importante scenario in cui è utile l’utilizzo del modello distribuito è quello in cui si debbano realizzare applicazioni che si interfacciano con codice legacy: in tal caso si può pensare di inglobare gli applicativi esistenti (legacy appunto) in oggetti remoti e pilotarne in tal modo le funzionalità da un client. Tutto ciò che veniva richiesto a un programma per diventare un componente di un sistema distribuito era di essere compilato e collegato con il corretto set di librerie RPC. Lo sviluppo di una applicazione distribuita con le RPC è basato su una metodologia ben definita. Assumiamo di voler sviluppare un server che implementi una procedura che debba essere usata in remoto da un singolo client. Il primo passo è quello di definire l’interfaccia della procedura. Questo viene fatto con un IDL, che fornisce una rappresentazione astratta della procedura in termini di quali parametri essa prende in ingresso e quali parametri restituisce. Tale descrizione IDL può essere considerata la specifica dei servizi offerti dal server. Una volta ottenuta la descrizione IDL si può procedere nello sviluppo del client e del server. Il secondo passo consiste nel compilare la descrizione IDL. Ogni implementazione delle RPC e ogni middleware che utilizza le RPC fornisce un compilatore di interfaccia. Osservando la Figura 1.1 possiamo osservare che la compilazione di una interfaccia IDL produce: Client Stub: lo stub è un pezzo di codice da compilare e collegare con il client. Quando il client chiama una procedura remota, la chiamata che viene realmente eseguita è una chiamata locale alla procedura fornita dallo stub. Lo stub allora si preoccupa di localizzare il server, formattare i dati in modo opportuno (il che implica marshaling e serializzazione1 , comunicare con il server, ottenere una risposta, e inoltrare tale risposta come parametro di ritorno della procedura invocata dal 1 il marshaling è l’operazione di trasformazione dei dati in un formato intermedio sul quale i diversi processi remoti si sono accordati o che è stato stabilito in una specifica rilasciata da un consorzio internazionale (vedi XDR). Infatti, i vari processi remoti possono girare eventualmente su differenti piattaforme. In questo modo il messaggio può essere compreso da ogni ricevente; la serializzazione consiste nel trasformare il messaggio e i suoi 8 1.1 Dalle RPC a RMI (Object Broker) codice client processo client interfaccia nel linguaggio client client stub ambiente di sviluppo IDL sorgenti IDL compilatore IDL 9 processo server codice server interfaccia nel linguaggio server server stub moduli di supporto Figura 1.1: Sviluppo di applicazioni distribuite con RPC client. In altre parole, lo stub è un proxy per la reale procedura implementata al server. Lo stub fa apparire la procedura come una normale procedura locale (dal momento che è una parte di codice del client). Lo stub, comunque, non implementa la procedura, ma tutti i meccanismi necessari a interagire con il server remoto, allo scopo di eseguire quella particolare procedura. Server Stub: la natura del server stub è simile a quella del client stub eccetto che per il fatto che esso implementa il lato server dell’invocazione. Esso cioè contiene il codice per ricevere l’invocazione dal client stub, formattare i dati in modo opportuno (in modo speculare rispetto al client stub, comporta deserializzazione e unmarshaling), invocare la reale procedura implementata nel server, e inoltrare i valori di ritorno della procedura al client stub. Cosı̀ come il client stub, anche il server stub deve essere compilato e collegato con il codice del server. Le RPC hanno visto il massimo del loro successo nei sistemi Unix e sono strettamente legate al concetto di processo, ma male si inseriscono nel contesto del paradigma basato sugli oggetti. È questo il motivo principale alla base dell’esigenza di una tecnologia apposita, come RMI, per la gestione di oggetti distribuiti. La Remote Method Invocation (RMI) consente di complessi parametri in una stringa di byte prima di mandare il messaggio in un canale di comunicazione 1.2 La serializzazione e la trasmissione degli oggetti realizzare applicazioni distribuite in cui un programma A è in grado di invocare i metodi di un oggetto B in esecuzione su un computer remoto. In realtà il panorama della progettazione e gestione di oggetti distribuiti offre valide alternative, come ad esempio CORBA: la scelta può ricadere su RMI nel caso in cui si voglia implementare, in maniera semplice e veloce, una struttura a oggetti distribuiti full-Java (sia il lato client che quello server devono essere realizzati obbligatoriamente utilizzando tale linguaggio). 1.2 La serializzazione e la trasmissione degli oggetti Il meccanismo base utilizzato da RMI per la trasmissione dei dati fra client e server è quello della serializzazione: è quindi sicuramente utile soffermarsi su questo importante sistema di trasmissione prima di affrontare nello specifico la Remote Method Invocation. Grazie all’estrema semplicità con la quale essa permette il flusso di dati complessi all’interno di uno stream, la serializzazione spesso viene utilizzata anche indipendentemente da applicazioni RMI, e quindi quanto verrà qui detto resta di utilità generale. L’obiettivo principale della serializzazione è permettere la trasformazione in modo semplice di oggetti e strutture di oggetti in sequenze di byte manipolabili con i vari stream del package java.io. Ad esempio, grazie alla serializzazione è possibile inviare strutture dati di complessità arbitraria tramite un socket (utilizzando gli stream associati al socket stesso), oppure salvarli su file al fine di mantenere la persistenza. La scrittura su stream avviene mediante il metodo writeObject() appartenente alla classe ObjectOutputStream. Ad esempio, volendo salvare su file un’istanza di una ipotetica classe Record, si potrebbe scrivere: Record record = new Record(); FileOutputStream fos = new FileOutputStream("data.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(record); dove si è salvato su file binario (data.ser ) un oggetto di tipo Record. L’operazione, in questo caso, è stata fatta in due fasi: creazione di uno stream di serializzazione prima, e associazione di tale stream a un comune FileOutputStream. In modo altrettanto semplice si può effettuare l’operazione opposta che permette la trasformazione da stream a oggetto: FileInputStream fis = new FileInputStream("data.ser"); ObjectInputStream ois = new ObjectInputStream(fis); 10 1.2 La serializzazione e la trasmissione degli oggetti public class Record implements Serializable { private String firstName; private String lastName; private int phone; public Record (String firstName,String lastName,int phone) { this.firstName = firstName; this.lastName = lastName; this.phone = phone; } } Listato 1.1: Uso dell’interfaccia Serializable record = (Record)ois.readObject(); ois.close(); In questo caso si utilizza la classe ObjectInputStream e il metodo readObject(), il quale restituisce un oggetto di tipo Object, rendendo necessaria una operazione di conversione (cast esplicito). In entrambi i casi le operazioni di lettura e scrittura devono essere inserite in appositi blocchi try-catch al fine di prevenire possibili problemi di lettura scrittura o di conversione. Per poter serializzare un oggetto, un gruppo di oggetti, una struttura di complessità arbitraria, si utilizza sempre la medesima procedura e non ci sono particolari differenze di cui tener conto, a patto che l’oggetto sia serializzabile: per rispettare questo vincolo, un oggetto deve implementare l’interfaccia Serializable. L’interfaccia Serializable è vuota e non definisce alcun metodo; il suo scopo è esclusivamente quello di dichiarare la possibilità per l’oggetto di essere serializzato. Per questo, ad esempio, l’oggetto Record visto nell’esempio di cui sopra potrebbe essere definito come nel Listato 1.1. La regola della serializzazione è ricorsiva, per cui un oggetto, per essere serializzabile, deve contenere esclusivamente riferimenti a oggetti serializzabili. La maggior parte delle classi contenute all’interno del JDK è serializzabile, fatta eccezione per alcuni casi particolari: non sono serializzabili tutte le classi che inglobano al loro interno strutture dati binarie dipendenti dalla piattaforma, come ad esempio molti degli oggetti dell’API JDBC. In questo caso infatti i vari oggetti contengono al loro interno puntatori a strutture dati o codice nativo utilizzato per la comunicazione con lo strato driver del database. Per sapere se un dato oggetto sia serializzabile o meno si può utilizzare il tool Serial Version Inspector (comando serialver), messo a disposizione dal JDK, passando ad esso il nome completo della classe da analizzare. Ad esempio, 11 1.2 La serializzazione e la trasmissione degli oggetti 12 per verificare che la classe java.lang.String sia serializzabile si può scrivere da linea di comando la seguente istruzione: serialver java.lang.String che restituisce il serialVersionUID dell’oggetto: java.lang.String: static final long serialVersionUID = -6849794470754667710L; Invece tramite: serialver -show si manda in esecuzione la versione con interfaccia grafica di tale strumento (Figura 1.2). Figura 1.2: Versione grafica Benché il suo utilizzo sia relativamente semplice, la serializzazione nasconde alcuni aspetti importanti relativamente alla trasmissione degli oggetti. Per quanto visto finora si potrebbe immaginare che la serializzazione permetta la trasmissione di oggetti per mezzo di stream: in realtà questa concezione è quanto mai errata, dato che a spostarsi sono solamente le informazioni che caratterizzano un’istanza di un particolare oggetto. Ad esempio, durante la trasmissione in un socket l’oggetto non viene mai spostato fisicamente, ma ne viene inviata solo la sua rappresentazione e, successivamente, viene ricreata una copia identica dall’altra parte del socket: al momento della creazione di questa copia, il runtime creerà un oggetto nuovo, riempiendo i suoi dati con quelli ricevuti dal client. Risulta ovvio quindi che, al fine di consentire questo spostamento virtuale, su entrambi i lati, sia server che client, debba essere presente il codice relativo all’oggetto: il runtime quindi deve poter disporre dei file “.class” necessari per istanziare l’oggetto, o deve poterli reperire in qualche modo. Il serialVersionUID della classe serve proprio per identificare di quale tipo di oggetto siano i dati prelevati dallo stream. Si tenga presente che nella trasmissione delle informazioni relative all’oggetto sono inviati solamente quei dati realmente significativi della particolare istanza. Per questo 1.3 Architettura RMI non vengono inviati i metodi (che non cambiano mai), le costanti, le variabili con specificatore static, che formalmente sono associate alla classe e non alla istanza, e quelle identificate con la parola chiave transient. Tale keyword qualifica una variabile non persistente, ovvero una variabile il cui valore non verrà inviato nello stream durante la serializzazione. Il valore assunto da una variabile di questo tipo dipende da come essa è stata definita. Ad esempio, supponendo di scrivere: transient Integer Int = new Integer(10); al momento della deserializzazione, alla variabile Int verrà impostato il valore 10. Se invece si fosse scritto: transient Integer Int; durante la fase di deserializzazione, Int assumerebbe il proprio valore di default che per tutte le variabili di tipo reference è null, mentre per i tipi primitivi corrisponde al valore base (0 per gli int, false per i boolean, 0.0 per i float e cosı̀ via). Riconsiderando l’esempio visto in precedenza, la classe Record viene serializzata e deserializzata su uno stream. In questo caso, il processo di trasformazione da oggetto a sequenza di byte è effettuato utilizzando le procedure standard di conversione della JVM. Anche gli oggetti contenuti all’interno di Record sono trasformati in modo ricorsivo, utilizzando le medesime tecniche. 1.3 Architettura RMI In Figura 1.3 è riportata la struttura tipica di una applicazione RMI: è possibile notare come essa sia organizzata orizzontalmente in strati sovrapposti, e in due moduli verticali paralleli fra loro. Questa suddivisione verticale vede da una parte il lato client, e dall’altra il server: il primo è quello che contiene l’applicazione che richiede il servizio di un oggetto remoto, che a sua volta diviene il servente del servizio RMI. Lo strato più alto del grafico è costituito su entrambi i lati (client e server) da una applicazione eseguita sulla Java Virtual Machine in esecuzione su quel lato: nel caso del client si tratta di una applicazione che effettua le chiamate ai metodi di oggetti remoti, i quali poi sono eseguiti dall’applicazione remota. Questa ha quindi un ciclo di vita indipendente dal client che di fatto ignora la sua presenza. Subito sotto il livello applicazione troviamo i due elementi fondamentali dell’architettura RMI, ovvero lo stub e lo skeleton. Questi due oggetti forniscono una duplice rappresentazione dell’oggetto remoto: lo stub rappresenta una simulazione locale sul client dell’oggetto remoto che però, grazie 13 1.3 Architettura RMI allo skeleton, vive e viene eseguito sul lato server; i due elementi quindi non sono utilizzabili separatamente. Da un punto di vista funzionale, il client, Figura 1.3: Architettura RMI dopo aver ottenuto un reference dell’oggetto remoto (lo stub di tale oggetto), ne esegue i metodi messi a disposizione per l’invocazione remota, in modo del tutto analogo al caso in cui l’oggetto sia locale. Si può quindi scrivere: OggettoRemoto.nomeMetodo(); Da un punto di vista sintattico non vi è quindi nessuna differenza fra un oggetto locale e uno remoto. In conclusione il client (intendendo sia il programma che il programmatore della applicazione lato client) non ha che in minima parte la percezione di utilizzare un oggetto remoto. Uno dei grossi vantaggi nell’utilizzo di RMI consiste nella semplicità con cui si possono passare parametri durante l’invocazione dei metodi remoti e riceverne indietro i risultati; senza nessuna differenza rispetto al caso locale si può scrivere: Ris = OggettoRemoto.nomeMetodo(param_1, ..., param_n); Riconsiderando lo schema riportato nella figura 1.3, si ha che i vari parametri vengono serializzati dalla virtual machine del client, inviati sotto forma di stream al server, il quale li utilizzerà in forma deserializzata per utilizzarli all’interno del corpo del metodo invocato. Anche il durante percorso inverso, ovvero quello che restituisce un risultato al client, viene effettuata una serializzazione e quindi una deserializzazione dei dati. 14 1.3 Architettura RMI 15 Il procedimento, che da un punto di vista teorico risulta essere piuttosto complesso, è molto semplice da utilizzare. L’unico vincolo di cui si deve tener conto è che i parametri passati e il risultato ricevuto siano oggetti serializzabili, cioè devono implementare l’interfaccia Serializable. Il lato server e client sono collegati col sottostante Remote Reference Layer (RRL) che a sua volta si appoggia al Transport Layer (TL). Il primo dei due ha il compito di instaurare un collegamento logico fra i due lati, di codificare le richieste del client, inviarle al server, decodificare le richieste e inoltrarle allo skeleton. Ovviamente nel caso in cui quest’ultimo fornisca dei risultati per il particolare tipo di servizio richiesto, il meccanismo di restituzione di tali valori avviene in maniera del tutto simile ma in senso opposto. Al livello RRL viene instaurato un collegamento virtuale fra i due lati client e server, mentre fisicamente la connessione avviene al livello sottostante, il Transport Layer. Tale collegamento è di tipo sequenziale ed è per questo che si richiede la serializzazione dei parametri da passare ai metodi. Il collegamento virtuale dello strato RRL si basa su un protocollo di comunicazione generico e indipendente dal particolare tipo di stub o skeleton utilizzati: questa genericità permette di mantenere la massima indipendenza dal livello stub/skeleton, tanto che è possibile sostituire il RRL con versioni successive più ottimizzate. Il protocollo di conversione delle invocazioni dei metodi, l’impacchettamento dei riferimenti ai vari oggetti, e tutto quello che concerne la gestione a basso livello sono operazioni a carico sia dello strato RRL, sia e soprattutto dal TL, in cui si perde la concezione di oggetto remoto e/o locale e i dati vengono semplicemente visti come sequenze di byte da inviare o leggere verso certi indirizzi di memoria. Quando il TL riceve una richiesta di connessione da parte del client, localizza il server RMI relativo all’oggetto remoto richiesto: successivamente viene eseguita una connessione per mezzo di un socket appositamente creato per il servizio. Si tenga presente che, quando un client vuole chiamare un metodo, serializza i parametri e li invia sul canale. Il server deserializza i parametri ricevuti con l’invocazione e ricostruisce una copia locale dell’oggetto. Proprio per questo meccanismo, l’oggetto lato client e la sua copialato server sono indipendenti. Quindi una modifica sullo stato interno (ad esempio, le variabili di istanza) di uno dei due non si ripercuote sull’altro. In questo contesto, particolarmente importante è la presenza di un garbage collector apposito per RMI, il quale provvede in modo automatico e trasparente a eseguire le opportune operazioni di ripulitura delle aree di memoria non più utilizzate. Per ogni oggetto RMI remoto, il reference layer del server mantiene la lista dei riferimenti remoti che sono registrati dai client. Ogni riferimento remoto di ogni client è stato ottenuto esplicitamente attraverso una “lookup” o implicitamente come risultato dell’invocazione di un metodo remoto. Il client 1.4 Uso di RMI mantiene questi riferimenti nel suo Reference Layer. Quando la Java Virtual Machine del client si accorge che un oggetto remoto non è più referenziato localmente, questa notifica in modo asincrono al server RMI l’evento “unreferenced” cosicchè il server può aggiornare la lista dei riferimenti. Quando un oggetto RMI non ha più riferimenti remoti nella lista, la possibilità di tale oggetto di essere “garbage collected” è basata solamente sull’esistenza di riferimenti locali, come ogni altro oggetto java. Il distributed garbage collector associa ad ogni riferimento remoto un “lease”, cioè una sorta di timeout che può scadere. Quando il “lease” scade, il riferimento è eliminato dalla lista e viene inviato una richiesta di “lease renew” al reference layer del client che utilizzava tale riferimento. In questo modo al client viene notificato che il riferimento remoto è scaduto. A questo punto il client notifica al server RMI di mantenere ancora il riferimento remoto nella lista perchè valido. L’obiettivo del meccanismo di “lease renewal” è di permettere al server di accorgersi della terminazione anormale dei client e quindi di eliminare i riferimenti remoti non utili. Infatti, se non si facesse uso dei “lease”, un client andato in “crash” non manderebbe mai il messaggio appropriato di “unreferenced” prima di terminare l’esecuzione. In questo contesto, un client che invoca una System.exit() è considerato terminato in maniere anormale perchè tale operazione non permette al reference layer del client di mandare l’appropriato messaggio “unreferenced” al server. L’ultimo livello che però non viene incluso nella struttura RMI, è quello che riguarda la gestione della connessione al livello di socket e protocolli TCP/IP. Questo aspetto segue le specifiche standard di networking di Java e non offre particolari interessanti in ottica RMI. 1.4 Uso di RMI Si può ora procedere ad analizzare quali siano i passi necessari per realizzare una applicazione RMI. Tutte le classi e i metodi che si analizzeranno e, in generale, tutte le API necessarie per lavorare con RMI sono contenute nei package java.rmi e java.rmi.server. Anche se, dal punto di vista della programmazione a oggetti, sarebbe più corretto parlare di classi, in questo caso si parlerà genericamente di oggetti remoti e locali intendendo sia il tipo che la variabile. A tal proposito, in base alla definizione ufficiale, si definisce remoto un oggetto che implementi l’interfaccia Remote e i cui metodi possano essere eseguiti da una applicazione client non residente sulla stessa macchina virtuale. Un’interfaccia remota invece rende disponibile il set di metodi utilizzabili 16 1.4 Uso di RMI per l’invocazione a distanza; ovviamente non è necessario definire nell’interfaccia quei metodi a solo uso interno della classe. Si immagini quindi di definire MyServer un oggetto per il momento non remoto: public class MyServer { public void String concat(String a, String b) { return a + b; } } Il metodo concat() in questo caso esegue una concatenazione fra i due argomenti passati in input restituendo in uscita la stringa risultante. A parte il vincolo della serializzabilità dei parametri, non ci sono limiti alla complessità delle operazioni eseguibili all’interno di metodi remoti. Dopo aver definito questa semplice classe, per trasformarla nella versione remota si deve per prima cosa definire la sua interfaccia remota: public interface MyServerInterface extends Remote { public String concat(String a, String b) throws RemoteException; } Come si può osservare da queste poche righe di codice, per definire un’interfaccia remota è necessario estendere la java.rmi.Remote: questa è una interfaccia vuota e serve solo per verificare durante l’esecuzione che le operazioni di invocazione remota siano plausibili. Un oggetto utilizzato come parametro di input o di ritorno può implementare l’interfaccia Remote invece che quella Serializable. La differenza principale tra oggetti remoti e serializzabili è che gli oggetti remoti sono inviati come riferimento, mentre gli oggetti non remoti, serializzabili sono inviati come copia. Se l’oggetto è serializzabile ma non remoto esso viene serializzato e inviato al processo remoto come flusso di byte. È obbligatoria la gestione dell’eccezione java.rmi.RemoteException: infatti, a causa della distribuzione in rete, oltre alla gestione di eventuali problemi derivanti dalla normale esecuzione del codice (bug o incongruenze di vario tipo), si deve adesso proteggere tutta l’applicazione da anomalie derivanti dall’utilizzo di risorse remote: ad esempio potrebbe venire a mancare improvvisamente la connessione fisica verso l’host dove è in esecuzione il server RMI. Definita l’interfaccia remota si deve modificare leggermente la classe di partenza, in modo che implementi questa interfaccia, come mostrato nel Listato 1.2. Il nome della classe è stato cambiato a indicare l’implementazione 17 1.4 Uso di RMI public class MyServerImpl implements MyServerInterface extends UnicastRemoteObject { public MyServerImpl() throws RemoteException { ... } public String concat(String a, String b) throws RemoteException{ return a + b; } } Listato 1.2: Uso dell’interfaccia MyServerInterface che estende Remote dell’interfaccia remota; come si può notare, oltre a dichiarare di implementare l’interfaccia precedentemente definita, si deve anche estendere la classe UnicastRemoteObject. Oltre a ciò, all’oggetto è stato aggiunto un costruttore di default il quale dichiara di propagare eccezioni RemoteException: tale passaggio non ha una motivazione apparente, ma è necessario per permettere al compilatore di creare correttamente tutte le parti che compongono il lato server (stub e skeleton). La classe UnicastRemoteObject deriva dalle due classi, RemoteServer e RemoteObject: la prima è una superclasse comune per tutte le implementazioni di oggetti remoti (la parola Unicast ha importanti conseguenze come si avrà modo di vedere in seguito), mentre l’altra semplicemente ridefinisce hashcode() ed equals() in modo da permettere correttamente il confronto tra oggetti remoti. L’uso della classe RemoteServer permette di utilizzare implementazioni di oggetti remoti diverse da UnicastRemoteObject, anche se per il momento quest’ultima è l’unica supportata. L’organizzazione delle più importanti classi per RMI è raffigurata in Figura 1.4. Dopo questa trasformazione, l’oggetto è visibile dall’esterno, ma ancora non utilizzabile secondo la logica RMI: si devono infatti creare i cosiddetti stub e skeleton. Tali oggetti sono ottenibili in maniera molto semplice per mezzo del compilatore rmic, disponibile all’interno del JDK 1.1 e successivi: partendo dal bytecode ottenuto dopo la compilazione dell’oggetto remoto, questo tool produce stub e skeleton. Ad esempio, riconsiderando il caso della 18 1.4 Uso di RMI Figura 1.4: Struttura gerarchica delle principali classi e interfacce RMI classe MyServerImpl, con una operazione del tipo: rmic MyServerImpl si ottengono i due file MyServerImpl stub.class e MyServerImpl skel.class. A questo punto si hanno a disposizione tutti i componenti per utilizzare l’oggetto remoto MyServerImpl: resta quindi da rendere possibile il collegamento tra client e server per l’invocazione remota. Si definisce server RMI l’applicazione che istanzia un oggetto remoto e lo registra tramite una bind all’interno dell’RMI registry. Il server RMI non è quindi l’oggetto che implementa la business logic, ma solamente una applicazione di servizio necessaria per attivare il meccanismo di invocazione remota. Sul lato server, l’applicazione che gestisce lo skeleton deve notificare di possedere al suo interno un oggetto abilitato all’invocazione remota. Per far questo è necessario utilizzare il metodo statico java.rmi.Naming.bind() che associa all’istanza dell’oggetto remoto un nome logico con cui tale oggetto può essere identificato in rete. Quindi, dopo aver creato una istanza dell’oggetto remoto tramite: MyServerImpl server = new MyServerImpl(); si provvede a effettuarne la registrazione utilizzando un nome simbolico: 19 1.4 Uso di RMI import java.rmi.*; import java.rmi.server.*; public class Registration { public static void main(String args[]) { try { MyServerImpl server =new MyServerImpl(); Naming.bind("Paperino", server); } catch (Exception e) { System.out.println("Implerr:Ã" + e.getMessage()); e.printStackTrace(); } } } Listato 1.3: Registrazione dell’oggetto remoto Naming.bind("Paperino", server); Questa operazione, detta registrazione, può fallire e in tal caso viene generata una eccezione in funzione del tipo di errore. In particolare si otterrà una AlreadyBoundException nel caso in cui il nome logico sia già stato utilizzato per un’altra associazione, una MalformedURLException per errori nella sintassi dell’URL, mentre il runtime produrrà RemoteException per tutti gli altri tipi di errore legati alla gestione da remoto dell’oggetto. Ogni associazione nome logico - oggetto remoto è memorizzata in un apposito registro detto RMI registry. In questo caso rmiregistry è anche il comando che lancia l’applicazione per gestire tale archivio, applicazione che deve essere lanciata sul lato server prima di ogni bind. Una volta che l’RMI registry è attivo, è possibile registrare l’oggetto. Il codice completo per registrare l’oggetto è mostrato nel Listato 1.3. Il client a questo punto è in grado di ottenere un reference all’oggetto con una ricerca presso l’host remoto utilizzando il nome logico con cui l’oggetto è stato registrato. Ad esempio si potrebbe scrivere: MyServerInterface server; String url = "//" + serverhost + "/MyServer"; server = (MyServerInterface) Naming.lookup(url); 20 1.4 Uso di RMI 21 e quindi utilizzare il reference per effettuare le invocazioni ai metodi remoti: System.out.println(server.concat("HelloÃ", "world!")); Sul client per ottenere il reference si utilizza il metodo statico Naming.lookup(), che può essere considerato il corrispettivo alla operazione di bind sul server. L’URL passato come parametro al lookup() identifica il nome della macchina che ospita l’oggetto remoto e il nome con cui l’oggetto è stato registrato. Entrambe le operazioni di registrazione e di ricerca accettano come parametro un URL: il formato di tale stringa è la seguente: rmi://host:port/name dove host è il nome del server RMI, port la porta dove si mette in ascolto il registry, name il nome logico. Sul server non è necessario specificare l’host, dato che per default assume l’indirizzo della macchina stessa sulla quale l’applicazione server RMI è mandata in esecuzione. In entrambi i casi il numero della porta di default è la 1099 ma, se si specifica altrimenti, allora tale informazione dovrà essere passata al rmiregistry con il seguente comando: rmiregistry numero_porta Ogni volta che un parametro viene passato ad un metodo remoto, o viceversa, ogni volta che si preleva un oggetto come risultato di una computazione remota, si dà vita a un processo di serializzazione o deserializzazione dell’oggetto in questione. In realtà, come si è potuto vedere, l’oggetto serializzato non viene spostato dal client al server, ma vengono inviate nella rete solamente le informazioni necessarie per ricreare una copia dell’oggetto dal client al server (e viceversa). Questo significa che sia il client che il server devono poter disporre dello stesso bytecode relativo all’oggetto serializzato in modo da poterne ricreare l’istanza. La soluzione più semplice è copiare fisicamente i vari file “.class” sia sul server che sul client: in questo caso si potrà essere sicuri che le varie operazioni di serializzazione e deserializzazione potranno essere effettuate correttamente. Lo svantaggio di questa organizzazione risiede nel dovere ridistribuire tutti i file per ogni modifica delle varie classi. In alcuni casi questa soluzione è scomoda, se non addirittura impraticabile. RMI mette a disposizione un meccanismo molto potente che consente di scaricare dalla rete, tramite un file server HTTP, i file necessari per il funzionamento del client. Per ulteriori dettagli sul loading remoto delle classi si faccia riferimento a [1]. Se si ripensa per un momento alla modalità di pubblicazione di un oggetto remoto da parte del server RMI, si potrà osservare come la funzione di creazione e registrazione sia un compito totalmente a carico del server. Per 1.4 Uso di RMI 22 public interface Messaggio extends Remote { String getTesto() throws RemoteException; Date getData() throws RemoteException; String getMittente() throws RemoteException; } Listato 1.4: Interfaccia Messaggio una precisa scelta progettuale quindi, visto che la registrazione dell’oggetto avviene una volta sola, l’istanza dell’oggetto remoto sarà l’unica disponibile e quindi condivisa fra tutti i client possibili. Pensata per semplificare al massimo il lavoro del programmatore, in molti casi però questa soluzione risulta essere troppo rigida e non sufficiente per supportare architetture distribuite complesse. Una soluzione semplicistica potrebbe essere quella di istanziare un numero prefissato di oggetti per poter servire più client; ovviamente tale soluzione, oltre ad essere poco flessibile e per niente elegante, non risolve il problema della creazione “on demand” di oggetti remoti da parte del client. Questo tipo di problema, che ricade nella sfera del pooling degli oggetti, può essere risolto in RMI tramite due tecniche: una basata sull’utilizzo di particolari pattern progettuali (soluzione quindi non strettamente legata a RMI, ma di valenza generale) e una basata sull’utilizzo di una particolare interfaccia remota messa appositamente a disposizione per risolvere questo problema. Il framework EJB che, per certi versi, può essere considerato come l’evoluzione di RMI nasce per risolvere una serie di problemi presenti nei modelli di programmazione distribuita come RMI, ma anche CORBA. Infatti in EJB il problema del pooling degli oggetti viene risolto in modo molto potente ed elegante, demandando al container la gestione del numero degli oggetti remoti in esecuzione in base alle esigenze di sistema e alle richieste dei vari client. Il problema del pooling di oggetti può essere risolto utilizzando una tecnica introdotta in RMI ad hoc, basata su Java Activation, ma risulta essere alquanto complessa(vedi [1]). Invece, utilizzando il pattern Factory Method, si può ottenere lo stesso risultato in modo molto più semplice ed elegante. si supponga di avere una classe remota che implementi l’interfaccia mostrata nel Listato 1.4. L’implementazione di tale classe è mostrata nel Listato 1.5 In uno scenario reale si potrebbe ipotizzare che ogni client debba o voglia poter produrre messaggi propri, indipendentemente dagli altri, e che tali messaggi siano gestiti da un server centrale. Si noti come le proprietà siano 1.4 Uso di RMI 23 public class MessaggioImpl extends UnicastRemoteObject implements Messaggio{ String testo; Date data; String mittente; public MessaggioImpl(String unMittente,String unTesto) throws RemoteException{ super(); data=new Date(); testo=unTesto; mittente=unMittente; } public String getTesto() throws RemoteException { return(testo); } public Date getData() throws RemoteException { return(data); } public String getMittente() throws RemoteException { return(mittente); } } Listato 1.5: Classe MessaggioImpl 1.4 Uso di RMI 24 import java.rmi.*; import java.util.*; public interface Factory extends Remote { Messaggio creaMessaggio(String sndr, String text) throws RemoteException; } public class FactoryImpl extends UnicastRemoteObject implements Factory { public FactoryImpl() throws RemoteException { super(); } public Messaggio creaMessaggio(String sndr, String text) throws RemoteException { MessaggioImpl unMessaggio=new MessaggioImpl(sndr,text); return (unMessaggio); } } Listato 1.6: Interfaccia Factory immutabili e impostate nel costruttore; si noti anche come, addirittura, la proprietà Data sia in realtà impostata automaticamente alla data attuale del Sistema. Per fare questo, si deve predisporre un meccanismo che permetta la creazione di tali oggetti anche da parte di client RMI. La classe MessaggioFactory che implementa il pattern Factory, è a tutti gli effetti un oggetto remoto come quelli visti in precedenza: tramite la sua registrazione, ogni client potrà ottenerne lo stub e invocarne il metodo remoto getLogger(), la cui implementazione è riportata di seguito, nel Listato 1.6: Il client che vuole creare una nuova instanza della classe MessaggioImpl (che implementa l’interfaccia Messaggio) deve semplicemente chiamare il metodo creaMessaggio 1.5 Esempio: Sender e Receiver public class Application { public static void main(String args[]) { String sndr= ... String text= ... Factory fact= (Factory)Naming.lookup(...); Messaggio mex = fact.creaMessaggio(sndr,text); ... } } Listato 1.7: Classe Application (vedi Listato 1.7). La chiamata al metodo Factory creaMessaggio in realtà non fa nient’altro che creare un nuovo oggetto Messaggio e restituirne un riferimento remoto al client richiedente. Tale messaggio avrà per le proprietà Mittente e Testo i valori passati al metodo Factory e la proprietà Data impostato con la data del server (e non quella del client). Il client invocando il metodo remoto creaMessaggio() riceverà lo stub dell’oggetto remoto, il quale verrà eseguito sul server remoto, in perfetto accordo con il modello teorico di RMI. L’utilizzo del pattern Factory risolve in maniera piuttosto agile alcune delle limitazioni imposte dal modello RMI: con tale tecnica infatti è possibile attivare oggetti al momento della effettiva necessità su richiesta del client ed in modo esclusivo. 1.5 Esempio: Sender e Receiver Vediamo ora un esempio pratico di quanto detto finora. In questo esempio si vuole gestire una applicazione client/server per fornire un servizio di pubblicazione/lettura di messaggi di testo. L’interfaccia del servizio può essere definita con l’interfaccia Archivio che estende a sua volta l’interfaccia Java Remote(Listato 1.8). Tale interfaccia definisce l’interfaccia del servizio, espressa in linguaggio Java e quindi corrisponde all’IDL del servizio. In Archivio vengono dichiarati due metodi, send e receive, che devono essere definiti nella classe ArchivioImpl(implementazione del servizio), come mostrato nel Listato 1.8. Questa classe estende UnicastRemoteObject e implementa Archivio. Per gestire la lista dei messaggi, viene utilizzato un oggetto (coda) di tipo ArrayList. Il costruttore deve per prima cosa chiamare la superclasse. I due metodi, send e receive, utilizzano la variabile x come semaforo (syn- 25 1.5 Esempio: Sender e Receiver import java.rmi.*; public interface Archivio extends Remote { void send(String mex) throws RemoteException; String receive() throws RemoteException; } Listato 1.8: Definizione dell’interfaccia Archivio chronized ), consentono di accedere alla lista (sia in lettura che in scrittura) in modo consistente, evitando situazioni ambigue. Qualsiasi situazione di accesso concorrente (due letture, due scritture, una lettura e una scrittura) potrebbero generare situazioni non desiderate, come la perdita di messaggi. Grazie all’istruzione synchronized(x), se un client sta pubblicando o leggendo un messaggio, un altro client che vuole a sua volta leggere o pubblicarne uno deve attendere che il primo client abbia terminato l’operazione per poterne cominciare una. Il client si implementa con due classi, la classe Sender e la classe Receiver. Queste sono mostrate rispettivamente nel Listato 1.11 e nel Listato 1.12. Nelle classi Sender e Receiver viene passato come parametro la stringa args[0] che è il nome oppure l’indirizzo IP del server che fornisce il servizio. La stringa args[1] utilizzata nella classe Sender è invece il messaggio che si vuole inserire in coda. RMI non fornisce alcun meccanismo di serializzazione delle invocazioni dei metodi remoti. La serializzazione deve essere realizzata dal programmatore mediante l’uso di codice opportuno (ad esempio, i blocchi synchronized ). Questa limitazione è stata superata dalla naturale evoluzione di RMI, cioè gli EJB. 26 1.5 Esempio: Sender e Receiver import java.rmi.server.*; public class ArchivioImpl extends UnicastRemoteObject implements Archivio { ArrayList coda; String x=""; public ArchivioImpl() throws RemoteException { super(); coda=new ArrayList(); } public void send(String messaggio) throws RemoteException { synchronized(x) { coda.add(messaggio); } } public String receive() throws RemoteException { if (coda.isEmpty()) return(null); synchronized(x) { try { String messaggio=(String)coda.get(0); coda.remove(0); return(messaggio); } catch(NoSuchElementException err) { return(null); } } } } Listato 1.9: Implementazione del servizio definito dall’Interfaccia Archivio 27 1.5 Esempio: Sender e Receiver import java.rmi.*; import java.rmi.server.*; public class ArchivioServer { public static void main(String args[]) { try { ArchivioImpl obj=new ArchivioImpl(); Naming.rebind("Coda", obj); } catch (Exception e) { System.out.println("Implerr:Ã" + e.getMessage()); e.printStackTrace(); } } } Listato 1.10: Codice per la registrazione dell’oggetto nell’RMI Registry import java.rmi.*; public class Sender { public static void main(String args[]) throws Exception { if (args.length==0) { System.out.println("NessunÃmessaggioÃdaÃinviare"); return; } Archivio obj= (Archivio)Naming.lookup("//"+args[0]+"/Coda"); obj.send(args[1]); } } Listato 1.11: Classe Sender 28 1.5 Esempio: Sender e Receiver import java.rmi.*; public class Receiver { public static void main(String args[]) throws Exception { Archivio obj= (Archivio)Naming.lookup("//"+args[0]+"/Coda"); String messaggio=obj.receive(); if (messaggio==null) System.out.println("NessunÃmessaggioÃinÃcoda!"); else System.out.println(messaggio); } } Listato 1.12: Classe Receiver 29 Capitolo 2 Swing Uno dei problemi più grossi emersi durante la progettazione di Java fu senza dubbio la realizzazione di un toolkit grafico capace di funzionare con prestazioni di buon livello su piattaforme molto differenti tra loro. La soluzione adottata nel 1996 fu AWT(Abstract Window Toolkit), un package grafico che mappa i componenti del sistema ospite con apposite classi dette peer, scritte in gran parte in codice nativo. In pratica, ogni volta che il programmatore crea un componente AWT e lo inserisce in un’interfaccia grafica, il sistema AWT posiziona sullo schermo un oggetto grafico della piattaforma ospite, e si occupa di inoltrare ad esso tutte le chiamate a metodo effettuate sull’oggetto Java corrispondente, ricorrendo a procedure scritte in buona parte in codice nativo; nel contempo, ogni volta che l’utente manipola un elemento dell’interfaccia grafica, un’apposita routine (scritta sempre in codice nativo) crea un apposito oggetto Event e lo inoltra al corrispondente oggetto Java, in modo da permettere al programmatore di gestire il dialogo con il componente e le azioni dell’utente con una sintassi completamente Object Oriented e indipendente dal sistema sottostante. A causa di questa scelta progettuale, il set di componenti grafici AWT comprende solamente quel limitato insieme di controlli grafici che costituiscono il minimo comune denominatore tra tutti i sistemi a finestre esistenti: un grosso limite rispetto alle reali esigenze dei programmatori. In secondo luogo, questa architettura presenta un grave inconveniente: i programmi grafici AWT assumono un aspetto ed un comportamento differente a seconda della JVM su cui vengono eseguite, a causa delle macroscopiche differenze implementative esistenti tra le versioni di uno stesso componente presenti nelle diverse piattaforme. Spesso le interfacce grafiche realizzate su una particolare piattaforma mostrano grossi difetti se eseguite su un sistema differente, arrivando in casi estremi a risultare inutilizzabili. Il motto della Sun per Java era “scrivi (il codice) una volta sola ed eseguilo 30 2.1 Il package Swing 31 Component Container JComponent JPanel Window JApplet JFrame JDialog java.awt javax.swing Figura 2.1: Diagramma UML di base del package Swing. ovunque”; nel caso di AWT questo si era trasformato in “scrivi una volta sola e correggilo ovunque”. 2.1 Il package Swing Nel 1998, con l’uscita del JDK 1.2, venne introdotto il package Swing, i cui componenti erano stati realizzati completamente in Java, ricorrendo unicamente alle primitive di disegno più semplici, tipo “traccia una linea” o “disegna un cerchio”, accessibili attraverso i metodi dell’oggetto Graphics, un oggetto AWT utilizzato dai componenti Swing per interfacciarsi con la piattaforma ospite. Le primitive di disegno sono le stesse su tutti i sistemi grafici, e il loro utilizzo non presenta sorprese: il codice java che disegna un pulsante Swing sullo schermo di un PC produrrà lo stesso identico risultato su un Mac o su un sistema Linux. Questa architettura risolve alla radice i problemi di uniformità visuale, visto che la stessa identica libreria viene ora utilizzata, senza alcuna modifca, su qualunque JVM. Liberi dal vincolo del “minimo comune denominatore”, i progettisti di Swing hanno scelto di percorrere la via opposta, creando un package ricco di componenti e funzionalità spesso non presenti nella piattaforma ospite. Il procedimento di disegno è ovviamente più lento perché la JVM deve disegnare per proprio conto tutte le linee degli oggetti grafici e gestirne direttamente il comportamento, però è più coerente. Le classi Swing sono definite nel pacchetto javax.swing, il cui nome javax 2.2 Top Level Container indica estensione standard a Java. Inizialmente Swing venne rilasciato, infatti, come estensione per poi divenire un omponente standad di Java 2. Per motivi di compatibilita il nome del pacchetto javax non venne corretto in java. Gli oggetti grafici Swing derivano dai corrispettivi AWT; quindi è possibile utilizzare oggetti Swing, ove erano previsti i oggetti antenati. Swing usa ancora alcuni elementi AWT per disegnare; anche la gestione degli eventi è fatta per la maggior parte da classi AWT. La Figura 2.1 riassume molto sinteticamente le classi base del package Swing e come queste derivino da classi AWT. Ogni oggetto grafico (una finestra, un bottone, un campo di testo, . . . ) è implementato come classe del package javax.swing. Ogni classe che identifica un oggetto Swing deriva per lo più dalla classe javax.swing.JComponent; si stima che esistono circa 70 o più oggetti diversi. Gli oggetti grafici utilizzati per disegnare le interfacce vengono chiamati anche controlli oppure tecnicamente widget. JComponent eredita da java.awt.Container, una sorta di controllo che di default è vuoto e il cui scopo è offrire la possibilità di disporre altri componenti all’interno. Non a caso la classe AWT Window e le sottoclassi Swing JFrame e JDialog, le cui istanze rappresentano finestre, sono sottoclasse di Container. La cosa più sorprendente è che, siccome JComponent deriva da Container, è possibile inserire all’interno di un qualsiasi widget qualsiasi altro. Ad esempio - sebbene poco utile - è possibile aggiungere un campo di testo all’interno di un bottone oppure - molto usato - un Container all’interno di un altro Container. La classe Container (e ogni sottoclasse) definisce un metodo per aggiungere un controllo ad un Container: void add(Component); Il metodo prende come parametro un oggetto Component che è la superclasse di qualsiasi oggetto o container Swing o AWT. 2.2 Top Level Container I top level container sono i componenti all’interno dei quali si creano le interfacce grafiche: ogni programma grafico ne possiede almeno uno, di solito un JFrame, che rappresenta la finestra principale. Ogni top level container possiede un pannello (accessibile tramite il metodo getContentPane()) all’interno del quale vanno disposti i controlli dell’interfaccia grafca. Esistono tre tipi principali di top level Container: JFrame, JApplet e JDialog. Il primo viene solitamente usato come finestra principale per il programma, il secondo è utilizzato per costruire Applet da visualizzare nella finestra di un web browser mentre il terzo serve a creare le finestre di dialogo con l’utente. 32 2.2 Top Level Container 2.2.1 33 Uso di JFrame Un oggetto della classe JFrame può essere creato usando i costruttori: JFrame(); JFrame(String titoloFinestra); Il primo costruisce un JFrame senza titolo; il secondo permette di specificarlo. È sempre possibile impostare il titolo ricorrendo al metodo setTitle(String s). Due importanti proprietà dell’oggetto sono la dimensione e la posizione, che possono essere impostate sia specificando le singole componenti sia mediante oggetti Dimension e Point del package AWT: public public public public void void void void setSize(Dimension d); setSize(int width,int height); setLocation(Point p); setLocation(int x,int y); Ricorrendo al metodo setResizable(boolean b) è possibile stabilire se si vuole permettere all’utente di ridimensionare la finestra manualmente. Infine, vi sono tre metodi piuttosto importanti: public void pack(); public void setVisible(boolean b); public void setDefaultCloseOperation(int operation); Il primo ridimensiona la finestra tenendo conto delle dimensioni ottimali di ciascuno dei componenti presenti all’interno. Il secondo permette di visualizzare o di nascondere la finestra. Il terzo imposta l’azione da eseguire alla pressione del bottone close, con quattro impostazioni disponibili: JFrame.DO NOTHING ON CLOSE (nessun effetto), JFrame.HIDE ON CLOSE (nasconde la finestra), JFrame.DISPOSE ON CLOSE (chiude la finestra e libera le risorse di sistema) e JFrame.EXIT ON CLOSE (chiude la finestra e conclude l’esecuzione del programma). Per impostazione di default, un JFrame viene costruito non visibile e di dimensione 0 x 0. Per questa ragione, affinchè la finestra sia visibile, è necessario chiamare i metodi setSize() o pack() per specificare la dimensione e mettere a true la proprietà Visible chiamando il metodo: setVisible(true). Per poter lavorare con i Frame Swing, è opportuno conoscere il linea generale la struttura della superficie. La superficie di un frame Swing è coperta da quattro lastre: Glass Pane La lastra di vetro è nascosta di default ed ha il compito di catturare gli eventi di input sulla finestra. Normalmente è completamente trasparente a meno che venga implementato il metodo paintComponent 2.3 Paranoramica di alcuni widget Figura 2.2: Anatomia di un Frame Swing. del GlassPane. Poichè è davanti a tutte le altre, qualsiasi oggetto disegnato su questa lastra nasconde qualsiasi altro disegnato sulle altre Content Pane La lastra dei contenuti è la più importante perchè è quella che ospita i componenti che volete visualizzare nella finestra e la maggior parte dei programmatori Java lavora solo su questa Layered Pane Contiene la lastra dei contenuti ed eventualmente i menu. I menu, infatti, non vengono mai aggiunti al Content Pane ma a questa lastra. Root Pane La lastra radice ospita la lastra di vetro insieme con la lastra dei contenuti e i bordi della finestra. Per maggiori dettagli si faccia riferimento a [4]. Quindi, un componente, quale un pulsante, un’immagine o altro, non viene aggiunto direttamente alla finestra ma alla lastra dei contenuti. Di conseguenza, occorre per prima cosa procurarsi un riferimento all’oggetto ContentPane, mediante la chiamata al metodo: public Container getContentPane(); Come era da aspettarsi, il ContentPane “è un” Container perchè predisposto a contenere altri componenti. A questo punto, come per ogni altro Container, è possibile aggiungere ad esso un componente con il metodo add già descritto. A questo punto è possibile disegnare la prima finestra. Il codice descritto nel Listato 2.1 mostra la finestra in Figura 2.3. 2.3 Paranoramica di alcuni widget I nomi delle classi per la maggior parte dei componenti dell’interfaccia utente Swing iniziano con la J. 34 2.3 Paranoramica di alcuni widget import javax.swing.*; import java.awt.*; public class Application { public static void main(String args[]) { JFrame win; win = new JFrame("PrimaÃfinestra"); Container c = win.getContentPane(); c.add(new JLabel("BuonaÃLezione")); win.setSize(200,200); win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); win.setVisible(true); } } Listato 2.1: La prima finestra Figura 2.3: La prima finestra. 35 2.3 Paranoramica di alcuni widget 36 JTextField è un campo di testo Swing. Tale classe eredita da TextField, l’obsoleto analogo del package AWT. Per costruire un campo di testo occorre fornirne l’ampiezza, cioè il numero approssimato di caratteri che vi aspettate verranno inseriti dall’utente. JTextField xField=new JTextField(5); Gli utenti possono digitare anche un numero maggiore di caratteri ma sempre 5 contemporaneamente saranno vicini: i 5 attorno alla posizione del cursore nel campo. Sarebbe opportuno etichettare ciascun campo di testo in modo che l’utente sappia cosa scriverci. Ogni etichetta è un oggetto di tipo JLabel che viene costruito, sfruttando il costruttore con un parametro stringa; tale parametro rappresenta il testo dell’etichetta: JLabel xField=new JLabel("xÃ=Ã"); Figura 2.4: Alcuni Bottoni delle Swing. Inoltre vorrete dare all’utente la possibilità di inserire informazione in tutti i campi di testo prima di elaborarli per cui avete bisogno di un pulsante che l’utente possa premere per segnalare che i dati sono pronti per essere elaborati. Un pulsante è un oggetto JButton che può essere costruito fornendo una stringa che fungerò da etichetta, un’immagine come icona o entrambe JButton moveButton=new JButton("Move"); JButton moveButton=new JButton(new ImageIcon("hand.gif")); JButton moveButton=new JButton("Move",new ImageIcon("hand.gif")); JCheckBox è una sottoclasse di JButton che crea caselle di controllo, con un aspetto simile a quello delle caselle di spunta dei questionari. Il suo funzionamento è analogo a quello della superclasse, ma di fatto tende a 2.3 Paranoramica di alcuni widget essere utilizzato in contesti in cui si offre all’utente la possibilità di scegliere una o più opzioni tra un insieme, come avviene per esempio nei pannelli di controllo. I costruttori disponibili sono gli stessi della superclasse, quindi non sarà necessario ripetere quanto è stato già detto. JCheckBox check1=new JCheckBox("JCheck"); JRadioButton è una sottoclasse di JButton, dotata dei medesimi costruttori. Questo tipo di controllo, chiamato pulsante di opzione, viene usato tipicamente per fornire all’utente la possibilità di operare una scelta tra un insieme di possibilità, in contesti nei quali un’opzione esclude l’altra. I costruttori disponibili sono gli stessi della superclasse. Per implementare il comportamento di mutua esclusione, è necessario registrare i JRadioButton che costituiscono l’insieme presso un’istanza della classe ButtonGroup, come viene mostrato nelle righe seguenti: JRadioButton radioButton1=new JRadioButton("R1"); JRadioButton radioButton2=new JRadioButton("R2"); JRadioButton radioButton2=new JRadioButton("R3"); ButtonGroup group = new ButtonGroup(); group.add(radioButton1); group.add(radioButton2); group.add(radioButton3); Ogni volta che l’utente attiva uno dei pulsanti registrati presso il ButtonGroup, gli altri vengono automaticamente messi a riposo. I JComboBox offrono all’utente la possibilità di effettuare una scelta a partire da un elenco elementi, anche molto lungo. A riposo il componente si presenta come un pulsante, con l’etichetta corrispondente al valore attualmente selezionato. Un clic del mouse provoca la comparsa di un menu provvisto di barra laterale di scorrimento, che mostra le opzioni disponibili. Se si imposta la proprietà editable di un JComboBox a true esso si comporterà a riposo come un JTextField, permettendo all’utente di inserire valori non presenti nella lista. È possibile creare un JComboBox usando i seguenti costruttori: JComboBox(); JComboBox(Object[] items); Il secondo costruttore permette di inizializzare il componente con una lista di elementi di qualsiasi tipo (ad esempio String). Se viene aggiunto al ComboBox un oggetto generico (ad esempio un oggetto Libro), allora il valore corrispondente visualizzato nella lista delle opzioni è quello ottenuto chiamando sull’oggetto il metodo toString(). Quindi se si desidera aggiungere oggetti generici (magari definiti all’interno del programma), bisognerà 37 2.3 Paranoramica di alcuni widget import javax.swing.*; import java.awt.*; public class Application { public static void main(String args[]) { JFrame win; win = new JFrame("EsempioÃdiÃJComboBox"); String lista[]=new String[10]; for(int i=0;i<lista.length;i++) lista[i]="ElementoÃnumeroÃ"+i; JComboBox cBox=new JComboBox(lista); Container c = win.getContentPane(); c.add(cBox); win.setSize(200,200); win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); win.setVisible(true); } } Listato 2.2: Esempio di uso dei ComboBox avere la cura di ridefinire tale metodo. Un gruppo di metodi permette di aggiungere, togliere o manipolare gli elementi dell’elenco, cosı̀ come si fa con un Vector: public public public public public public public void addItem(Object anObject); void removeItem(Object anObject) void removeItemAt(int anIndex); void removeAllItems(); Object getItemAt(int index); int getItemCount(); void insertItemAt(Object anObject, int index) Per ottenere l’elemento correntemente selezionato, è disponibile il metodo: public Object getSelectedItem(); La Figura 2.5 mostra un esempio di suo uso. Il Listato 2.2 rappresenta il codice corrispondente: 38 2.4 L’ereditarietà per personalizzare i frame Figura 2.5: Uso di ComboBox. 2.4 L’ereditarietà per personalizzare i frame Aggiungendo ad un frame molti componenti dell’interfaccia utente, il frame stesso può diventare abbastanza complesso: per frame che contengono molti componenti è opportuno utilizzare l’ereditarietà. Infatti, se il software che si sta sviluppando contiene molte finestre ricche di componenti ci si troverebbe a dover fronteggiare una soluzione il cui caso limite è di un metodo main che contiene la definizione di tutte le finestre del programma. Senza arrivare a questa situazione, si otterrebbero moduli sono molto accoppiati e poco coesi; inoltre, gli stessi moduli sarebbero poco leggibili e non sarebbe facile manutenerli. In parole povere, non si sfrutterebbe le potenzialità dell’Object Orientation: non sarebbe possibile permette l’information hiding dei contenuti dei frame e dei controlli in essi contenuti; non sarebbe nemmeno sfruttato l’incapsulamento delle implementazione, mettendo insieme concetti eterogenei e finestre diverse tra loro. Il Listato 2.3 produce la stessa finestra in Figura 2.3 sfruttando l’ereditarietà. La differenza rispetto agli esempi già visti non è da poco: a questo punto ogni finestra diventa una nuova classe che, ereditando da JFrame, è un modello per le finestre. É opportuno definire gli oggetti che mappano i widgets come variabili di instanza (nel l’esempio l’unico widget è una etichetta (JLabel). La definizione della finestra e dei componenti contenuti viene fatta direttamente nel costruttore (o in metodo chiamati da questo). La prima istruzione chiama il costruttore della superclasse che prende come parametro una stringa; in questo modo è possibile impostare il titolo della finestra. Infatti, se non fosse stato esplicitamente chiamato il costruttore 39 2.4 L’ereditarietà per personalizzare i frame import javax.swing.*; import java.awt.*; class MyFrame extends JFrame { JLabel jl = new JLabel("BuonaÃLezione"); public MyFrame() { super("PrimaÃfinestra"); Container c = this.getContentPane(); c.add(jl); this.setSize(200,200); this.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); this.setVisible(true); } } public class Application { public static void main(String args[]) { MyFrame = new MyFrame(); } } Listato 2.3: Esempio con l’ereditarietà 40 2.5 I Layout Manager e la gerarchia di contenimento ad un parametro String, sarebbe stato chiamato implicitamente il costruttore della superclasse di default (quello senza parametri) prima di continuare l’esecuzione di quello della classe derivata. Il costruttore senza parametri avrebbe impostato a vuoto il titolo della finestra.1 Il resto del costruttore è simile alla tecnica di definizione delle finestre degli esempi precedenti con l’unica differenza che i metodi sono chiamati su this (cioè se stesso) perchè a questo punto i metodi da chiamare sono quelli ereditati dalla superclasse JFrame. 2.5 2.5.1 I Layout Manager e la gerarchia di contenimento Layout Management Quando si dispongono i componenti all’interno di un Container sorge il problema di come gestire il posizionamento: infatti, sebbene sia possibile specificare le coordinate assolute di ogni elemento dell’interfaccia, queste possono cambiare nel corso della vita del programma allorquando la finestra principale venga ridimensionata. In molti Container i controlli sono inseriti da sinistra verso destra con su una ipotetica riga: può non essere sempre la politica per la GUI (Graphic User Interface) desiderata. Per semplificare il lavoro di impaginazione e risolvere questo tipo di problemi possibile ricorrere ai layout manager, oggetti che si occupano di gestire la strategia di posizionamento dei componenti all’interno di un contenitore. Un gestore di layout è una qualsiasi classe che implementa l’interfaccia LayoutManager; ogni container nasce con un certo Layout Manager ma è possibile assegnare il più opportuno per quel Container con il metodo: public void setLayout(LayoutManager m); Il primo layout da essere citato è il gestore a scorrimento (Flow Layout) che sono inseriti da sinistra verso destra con la loro Preferred Size, cioè la dimensione minima necessaria a disegnarlo interamente. Usando il paragone con un editor di testi, ogni controllo rappresenta una “parola” che ha una sua propria dimensione. Come le parole in un editor vengono inseriti da sinistra verso destra finchè entrano in una riga, cosı̀ viene fatto per i controlli inseriti da sinistra verso destra. Quando un componente non entra in una “riga” viene posizionato in quella successiva. I costruttori più importanti di oggetti FlowLayout sono i seguenti: 1 A dire il vero, sarebbe stato possibile impostare il titolo della finestra in un secondo momento modificando la proprietà Title: setTitle(‘‘Prima Finestra’’); 41 2.5 I Layout Manager e la gerarchia di contenimento (a) 42 (b) Figura 2.6: Aspetto grafico del frame definito nel Listato 2.4 e comportamento del Flow Layout rispetto al ridimensionamento public FlowLayout(); public FlowLayout(int allin); Il secondo costruttore specifica l’allineamento dei controlli su una riga; il parametro può essere una delle seguenti costanti che rispettivamente allineano a sinistra, centro o destra: FlowLayout.LEFT FlowLayout.CENTER FlowLayout.RIGHT Il costruttore di default imposta l’allineamento centrale. Il Listato 2.4 mostra un esempio di uso del Flow Layout. L’aspetto del frame risultato è mostrato in Figura 2.6. É opportuno osservare come l’allineamento dei componenti del frame si adatti durante l’esecuzione quando la stessa finestra viene ridimensionata (vedi situazione 2.6(b)). Ovviamente se non esiste nessun allineamento per i componenti del frame tale che tutti siano contemporaneamente visibile, allora alcuni di questi risulteranno in parte o del tutto non visibili; per esempio, la finestra è troppo piccola. Il gestore a griglia (GridLayout) suddivide il contenitore in una griglia di celle di uguali dimensioni. Le dimensioni della griglia vengono definite mediante il costruttore: public GridLayout(int rows, int columns) in cui i parametri rows e columns specificano rispettivamente le righe e le colonne della griglia. A differenza di quanto avviene con FlowLayout, i componenti all’interno della griglia assumono automaticamente la stessa dimensione, dividendo equamente lo spazio disponibile. L’esempio descritto nel Listato 2.5 permette di illustrare il funzionamento di questo pratico layout manager; il risultato è in Figura 2.7. Il gestore a bordi (BorderLayout) suddivide il contenitore esattamente in cinque aree, disposte a croce, come nella Figura 2.8. Ogni zona può 2.5 I Layout Manager e la gerarchia di contenimento import javax.swing.*; import java.awt.*; public class MyFrame extends JFrame { JButton uno=new JButton("Uno"); JButton due=new JButton("Due"); JButton tre=new JButton("Tre"); JButton quattro=new JButton("Quattro"); JButton cinque = new JButton("Cinque"); public MyFrame() { super("FlowÃLayout"); Container c = this.getContentPane(); c.setLayout(new FlowLayout()); c.add(uno); c.add(due); c.add(tre); c.add(quattro); c.add(cinque); setSize(300,100); setVisible(true); } } public class Application { public static void main(String args[]) { MyFrame = new MyFrame(); } } Listato 2.4: Uso del Flow Layout 43 2.5 I Layout Manager e la gerarchia di contenimento public class MyFrame extends JFrame { public MyFrame() { super("GridÃLayout"); Container c = this.getContentPane(); c.setLayout(new GridLayout(4,4)); for(int i = 0; i<15; i++) c.add(new JButton(String.valueOf(i)); setSize(300,300); setVisible(true); } } Listato 2.5: Uso del Grid Layout Figura 2.7: Aspetto grafico del frame definito nel Listato 2.5. 44 2.5 I Layout Manager e la gerarchia di contenimento Figura 2.8: Le aree di una disposizione a bordi contenere uno ed un solo widget (o Container): un secondo widget inserito in una zona sostituisce il precedente. Se una o più zone non vengono riempite, allora i componenti nelle altre zone sono estesi a riempire le zone vuote. Il BorderLayout è il gestore di layout di default per la Lastra dei Contenuti di un JFrame. Il programmatore può decidere in quale posizione aggiungere un controllo utilizzando la variante presente nei Container: public void add(Component c,String s); dove il primo parametro specifica il componente da aggiungere e il secondo indica la posizione. I valori validi per il secondo parametro sono le costanti BorderLayout.NORTH, BorderLayout.SOUTH, BorderLayout.CENTER, BorderLayout.EAST e BorderLayout.WEST. Si osservi l’esempio nel Listato 2.6. 2.5.2 Progettazione della GUI con le gerarchie di contenimento I gestori di layout permettono maggiore versatilità nell’inserimento dei controlli nelle finestre. Tuttavia nella stragrande maggioranza delle situazioni un’intera finestra non può seguire un unico layout. Si prenda, ad esempio, in considerazione il frame in Figura 2.9. Non è difficile convincersi che la parte in alto contenente una sorta di “tastierino numerico” segue un GridLayout mentre i bottoni in basso seguono un FlowLayout centrato. Poichè ogni Container può seguire uno ed un unico layout, occorre predisporre più Container, uno per ogni zona che ha un layout differente. Ovviamente ad ogni Container possono essere aggiunti direttamente i controlli oppure altri Container. La lastra dei contenuti è il Container “radice” ed altri si possono aggiungere all’interno. I Container (insieme con i componenti contenuti) aggiunti in un altro Container si comporteranno come ogni altro widget la cui 45 2.5 I Layout Manager e la gerarchia di contenimento public class MyFrame extends JFrame { JButton nord = new JButton("Nord"); JButton centro = new JButton("Centro"); JButton ovest=new JButton("Ovest"); public MyFrame() { super("BorderÃLayout"); Container c = this.getContentPane(); c.setLayout(new BorderLayout()); c.add(nord,BorderLayout.NORTH); c.add(centro,BorderLayout.CENTER); c.add(ovest,BorderLayout.WEST); setSize(300,300); setVisible(true); } } Listato 2.6: Uso del Border Layout Figura 2.9: Un frame più complesso dimensione preferita è quella minima necessaria a visualizzare tutti i componenti all’interno. Per creare nuovi Container conviene utilizzare la classe 46 2.5 I Layout Manager e la gerarchia di contenimento javax.swing.JPanel che eredita da java.awt.Container. La forza di questa soluzione è data dall’alta modularità: è possibile usare un layout per il pannello interno e un altro layout per il ContentPane. Il pannello interno verrà inserito nella finestra coerentemente con il layout della lastra dei contenuti. Inoltre all’interno pannelli interni se ne possono inserire altri con loro layout e cosı̀ via, come nel gioco delle scatole cinesi. Il numero di soluzioni diverse sono praticamente infinite. Ad esempio per il frame in Figura 2.9 è possibile creare due contenitoripannelli JPanel: uno per contenere il “tastierino numerico” organizzato il GridLayout ed un altro il FlowLayout per i tre bottoni in basso. La lastra dei Contenuti viene lasciata in BorderLayout: al centro viene aggiunto il pannello tastierino ed a sud il pannello con i tre bottoni. 2.5.3 Progettazione top down di interfacce grafiche Durante la progettazione delle interfacce grafiche, può essere utile ricorrere a un approccio top down, descrivendo l’insieme dei componenti a partire dal componente più esterno per poi procedere a mano a mano verso quelli più interni. Si può sviluppare una GUI come quella dell’esempio precedente seguendo questa procedura: 1. Si definisce il tipo di top level container su cui si vuole lavorare (tipicamente un JFrame). 2. Si assegna un layout manager al content pane del JFrame, in modo da suddividerne la superficie in aree più piccole. 3. Per ogni area messa a disposizione dal layout manager è possibile definire un nuovo JPanel. Ogni sotto pannello può utilizzare un layout manager differente. 4. Ogni pannello identificato nel terzo passaggio può essere sviluppato ulteriormente, creando al suo interno ulteriori pannelli o disponendo dei controlli. Il risultato della progettazione può essere rappresentato con un albero della GUI. L’obiettivo di tale albero è mostrare come i Container (a partire dalla lastra dei contenuti ) sono stati suddivisi per inserire i componenti o altri Container. Ogni componente (o Container) è rappresentato da un nodo i cui figli sono i componenti (o i Container) contenuti all’interno e il padre è il componente che lo contiene. 47 2.5 I Layout Manager e la gerarchia di contenimento Figura 2.10: Un frame d’esempio Una volta conclusa la fase progettuale, si può passare a scrivere il codice relativo all’interfaccia: in questo secondo momento, è opportuno adottare un approccio bottom up, realizzando dapprima il codice relativo ai componenti atomici, quindi quello dei contenitori e infine quello del JFrame. Esempio di Progettazione Top-Down JFrame BorderLayout nordPnl FlowLayout infoLbl centroPnl GridLayout (2,1) opz1chk opz2chk sudPnl FlowLayout okBtn cancBtn Figura 2.11: Un frame più complesso Un esempio finale viene mostrato per riassumere quanto è stato finora detto. Si inizierà mostrando la progettazione top-down dell’interfaccia e si concluderá mostrandone la realizzazione bottom-up. Si consideri la semplice finestra in Figura 2.10. L’albero della GUI corrispondente è in Figura 2.11. Il codice per mostrare tale finestra è nel Listato 2.7. Si osservi l’invocazione del metodo pack() che sostituisce il metodo setSize(x,y) e che imposta la dimensione della finestra alla minima necessaria a visualizzare tutti i controlli. Inoltre si osservi come le due istruzioni successive hanno 48 2.5 I Layout Manager e la gerarchia di contenimento public class MyFrame extends JFrame { JPanel nordPnl = new JPanel(); JPanel centroPnl = new JPanel(); JPanel sudPnl = new JPanel(); JLabel infoLbl = new Label("Selezionare:"); JCheckBox opz1Chk = new JCheckBox("Opz1"); JCheckBox opz2Chk = new JCheckBox("Opz2"); JButton okBtn=new JButton("OK"); JButton cancBtn=new JButton("Annulla"); public MyFrame() { super("Esempio"); centroPnl.setLayout(new GridLayout(2,1)); centroPnl.add(opz1Chk); centroPnl.add(opz2Chk); nordPnl.add(infoLbl); sudPnl.add(okBtn); sudPnl.add(cancBtn); getContentPane().add(nordPnl,BorderLayout.NORTH); getContentPane().add(centroPnl,BorderLayout.CENTER); getContentPane().add(sudPnl,BorderLayout.SOUTH); pack(); Dimension dim= Toolkit.getDefaultToolkit().getScreenSize(); setLocation((dim.getWidth()-this.getWidth())/2, (dim.getHeight()-this.getHeight())/2); setVisible(true); } } Listato 2.7: Il codice della figura 2.10 49 2.5 I Layout Manager e la gerarchia di contenimento lo scopo di centrare la finestra sullo schermo. Il metodo getScreenSize() chiamato sulla classe Singleton 2 Toolkit del package java.awt restituisce un riferimento ad un oggetto java.awt.Dimension. Questo possiede due proprietà Width e Height che, nel caso specifico, conterranno la dimensione dello schermo in pixel. L’istruzione successiva sposta la finestra al centro dello schermo con il metodo setLocation(int x,int y) dove x, y sono le coordinate dell’angolo in alto a sinistra. 2 Una classe Singleton è tale che di essa esista sempre al più una instanza. Per maggiori dettagli si faccia riferimento a [5] (in ogni modo sarà anche argomento del corso). 50 2.6 La Gestione degli Eventi 2.6 La Gestione degli Eventi Figura 2.12: La gestione ad event delegation La gestione degli eventi grafici in Java segue il paradigma event delegation (conosciuto anche come event forwarding). Ogni oggetto grafico è predisposto ad essere sollecitato in qualche modo dall’utente e ad ogni sollecitazione genera eventi che vengono inoltrati ad appositi ascoltatori, che reagiscono agli eventi secondo i desideri del programmatore. L’event delegation presenta il vantaggio di separare la sorgente degli eventi dal comportamento a essi associato: un componente non sa (e non è interessato a sapere) cosa avverrà al momento della sua sollecitazione: esso si limita a notificare ai propri ascoltatori che l’evento che essi attendevano è avvenuto, e questi provvederanno a produrre l’effetto desiderato. Ogni componente può avere più ascoltatori per un determinato evento o per eventi differenti, come anche è possibile installare uno stesso ascoltatore su più componenti anche diversi a patto che entrambi possono essere sollecitati per generare l’evento. L’operazione di installare lo stesso ascoltatore su più controlli (che quindi si comporteranno nello stesso modo se sollecitati dallo stesso evento) è frequente: si pensi alle voci di un menu che vengono replicate su una toolbar per un più facile accesso ad alcune funzionalità frequenti 2.6.1 Implementazione dell’event delegation Il gestore delle finestre può generare un numero enorme di eventi (sono eventi muovere il mouse nella finestra, spostare o ridimensionare una finestra, scri- 51 2.6 La Gestione degli Eventi vere o abbandonare un campo di testo, . . . ). Molte volte si è interessati a pochi di questi eventi. Per fortuna, il programmatore può considerare solo gli eventi di cui ha interesse, gestendoli con un ascoltatore opportuno, ignorando completamente tutti gli altri che di default verranno gestiti come eventi vuoti. Quando accade un evento per il quale esiste un ricevitore, la sorgente dell’evento chiama i metodi da voi forniti nell ricevitore, dando, in un oggetto di una classe evento, informazioni più dettagliate sull’evento stesso. In generale sono coinvolte tre classi: La classe del ricevitore. Implementa una particolare interfaccia del tipo XXXListener tipica degli eventi di una certa classe. I metodi dell’interfaccia che la classe dell’ascoltatore implementa contengono il codice eseguito allo scatenarsi degli eventi di una classe che l’ascoltatore intercetta. Grazie al fatto che ogni ascoltatore è caratterizzato da una particolare interfaccia Java, qualsiasi classe può comportarsi come un ascoltatore, a patto che fornisca un’implementazione per i metodi definiti dalla corrispondente interfaccia listener L’origine dell’evento. É il componente che genera l’evento che si vuole gestire su cui si vuole “installare” l’ascoltatore. Ogni componente dispone per ogni evento supportato di due metodi: void addXXXListener(XXXListener ascoltatore); void removeXXXListener(XXXListener ascoltatore); La classe evento. Contiene le informazioni riguardanti le caratteristiche dell’evento generato. Gli oggetti di questa classe sono istanziati direttamente dai componenti che notificano eventi agli ascoltatori. Formalmente sono parametri di input dei metodi dell’interfaccia implementata dall’ascoltatore. Nel momento in cui il componente viene sollecitato, esso chiama gli ascoltatori in modalità callback, passando come parametro della chiamata un’apposita sottoclasse di Event. Queste classi contengono tutte le informazioni significative per l’evento stesso e possono essere utilizzate per modificare il comportamento dell’ascoltatore in base alle informazioni sull’evento scatenato Le classi che permettono di gestire gli eventi generati dai componenti Swing sono in gran parte gli stessi utilizzati dai corrispondenti componenti AWT, e si trovano nel package java.awt.event. Alcuni componenti Swing, tuttavia, non hanno un omologo componente AWT: in questo caso le classi necessarie a gestirne gli eventi sono presenti nel package javax.swing.event. 52 2.6 La Gestione degli Eventi 2.6.2 Un esempio: elaborare gli eventi del mouse Per chiarire il funzionamento, a prima vista complesso, della gestione degli eventi Swing, verra fatto un esempio. In particolare, vogliamo realizzare un meccanismo per “spiare” gli eventi del mouse su una finestra e stamparli. Una classe che desidera catturare gli eventi del mouse deve implementare l’interfaccia MouseListener definita nel package java.awt.event che è cosı̀ definita: public interface MouseListener { void mouseClicked(MouseEvent e); void mouseEntered(MouseEvent e); void mouseExited (MouseEvent e); void mousePressed(MouseEvent e); void mouseReleased(MouseEvent e); } dove MouseEvent è la corrispondente classe evento il cui scopo è dare informazioni aggiuntive sull’evento del mouse. Infatti, questa definisce due metodi che permettono di conoscere le coordinate del mouse allo scatenarsi dell’evento (i primi due) e un terzo metodo che permette di determinare quale bottone del mouse è stato premuto: int getX(); int getY(); int getModifiers() Nel codice del Listato 2.9 viene mostrata l’implementazione della “spia”. Si noti come la classe ascoltatore MouseSpy implementi l’interfaccia MouseListener e come la classe definisca due metodi vuoti: mouseEntered e mouseExited. La definizione dei due metodi vuoti ha lo scopo di dichiarare che gli eventi legati all’entrata e all’uscita del mouse dall’area della finestra devono essere ignorati. In effetti, ignorare gli eventi è già il comportamento di default e quindi tale dichiarazione è superflua. La ragione per cui è stata comunque dichiarata la gestione di tali eventi è legata al linguaggio Java: affinché una classe implementi una interfaccia, tale classe deve implentare tutti i metodi dell’interfaccia. E questo vale anche in questo caso: la classe MouseSpy deve implementare tutti i metodi dell’interfaccia MouseListener per poter dichiarare di implementarla. Nella Tabella 2.1 sono riassunti gli ascoltatori più importanti. Per ulteriori dettagli si consiglia di consultare la documentazione Java Swing [6]. 53 2.6 La Gestione degli Eventi import java.awt.event.*; import javax.swing.*; public class MouseSpy implements MouseListener { public void mouseClicked(MouseEvent e) { System.out.println ("ClickÃsuÃ("+e.getX()+","+e.getY()+")"); } public void mousePressed(MouseEvent e) { System.out.println ("PremutoÃsuÃ("+e.getX()+","+e.getY()+")"); } public void mouseReleased(MouseEvent e) { System.out.println ("RilasciatoÃsuÃ("+e.getX()+","+e.getY()+")") } public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} } public class MyFrame extends JFrame { public MyFrame() { super("MouseTest"); this.addMouseListener(new MouseSpy()); setSize(200,200); setVisible(true); } } Listato 2.8: Primo esempio della gestione degli eventi 54 2.7 La gestione degli eventi Azione ActionListener ComponentListener FocusListener KeyListener MouseMotionListener MouseListener TextListener WindowListener Definisce 1 metodo per ricevere eventi-azione Definisce 4 metodi per riconoscere quando un componente viene nascosto, spostato, mostrato o ridimensionato Definisce 2 metodi per riconoscere quando un componente ottiene o perde il focus Definisce 3 metodi per riconoscere quando viene premuto, rilasciato o battuto un tasto Definisce 2 metodi per riconoscere quando il mouse e trascinato o spostato Se ne è già parlato. . . Definisce 1 metodo per riconoscere quando cambia il valore di un campo testo Definisce 7 metodi per riconoscere quando un finestra viene attivata, chiusa, disattivata, ripristinata, ridotta a icona, ecc. Tabella 2.1: Gli ascoltatori più importanti 2.6.3 Uso di adapter nella definizione degli ascoltatori Alcuni componenti grafici generano eventi cosı̀ articolati da richiedere, per la loro gestione, ascoltatori caratterizzati da più di un metodo. Si è già visto che MouseListener, ne definisce tre. L’interfaccia WindowListener (vedi Listato 2.9) ne dichiara addirittura sette, ossia uno per ogni possibile cambiamento di stato della finestra. Si è detto che se si desidera creare un ascoltatore interessato a un solo evento (per esempio uno che intervenga quando la finestra viene chiusa), esso dovrà comunque fornire un’implementazione vuota anche dei metodi cui non è interessato. Nei casi come questo è possibile ricorrere agli adapter: classi di libreria che forniscono un’implementazione vuota di un determinato ascoltatore. Per esempio, il package java.awt.event contiene la classe WindowAdapter, che fornisce un’implementazione vuota di tutti i metodi previsti dall’interfaccia. Una sottoclasse di WindowAdapter, pertanto, è un valido WindowListener, con in più il vantaggio di una maggior concisione. Si veda l’esempio 2.10 2.7 55 La gestione degli eventi Azione La maggior parte dei componenti Swing generano eventi, noti come eventi azione, che possono essere catturati da un ActionListener. Essa definisce un 2.7 La gestione degli eventi Azione public interface WindowListener { public void windowOpened(WindowEvent e); public void windowClosing(WindowEvent e); public void windowClosed(WindowEvent e); public void windowIconified(WindowEvent e); public void windowDeiconified(WindowEvent e); public void windowActivated(WindowEvent e); public void windowDeactivated(WindowEvent e); } Listato 2.9: L’interfaccia MouseListener class WindowClosedListener extends WindowAdapter { public void windowClosed(WindowEvent e) { // codice da eseguire alla chiusura della finestra } } Listato 2.10: Esempio dell’uso degli Adapter unico metodo: public interface ActionListener { public void actionPerformed(ActionEvent ae); } L’oggetto ActionEvent da informazioni aggiuntive sull’evento generato. Ad esempio, definisce il metodo Object getSource() che restituisce l’oggetto che ha generato l’evento. La maggior parte dei widget java sono predisposti per generare eventi azione. Ad esempio quando si clicca su un bottone, si preme INVIO in un campo di testo, si seleziona una voce di un menu oppure si seleziona/deseleziona una voce di una checkBox o un radioButton, viene generato un evento azione. Si potrebbe obiettare che, la gestione del click di un bottone, potrebbe essere fatta usando un mouseListener. Il vantaggio di utilizzare un evento azione risiede nel fatto che è possibile cliccare su un bottone anche con la tastiera (anche se in quel caso non è un vero click!): l’utente con il tasto TAB potrebbe spostarsi sulla schermata, mettere il focus su un bottone e qui premere la barra spaziatrice per voler “cliccare” sul bottone. 56 2.8 Accedere dall’ascoltatore agli oggetti di una finestra Figura 2.13: La finestra ottenuta dal Listato 2.11 É possibile installare uno stesso ascoltatore, ad esempio, per un bottone della toolbar e una voce di menu. Il vantaggio risiede nel fatto che sia la voce del menu che il bottone hanno lo stesso ascoltatore invece che due ascoltatori separati che fanno la stessa cosa. Un primo esempio di uso degli eventi azione è nel Listato 2.11. Il comportamento è quello in Figura 2.13: quando l’utente seleziona uno dei bottoni della finestra MyFrame viene aperta una finestra di dialogo con il testo del bottone premuto. Su ogni bottone è installato lo stesso ascoltatore per gli eventi azione. Alla pressione di un bottone viene eseguito il metodo actionPerformed, il quale si fa restituire con il metodo getSource() il bottone premuto (si noti il cast giacchè il metodo lo restituisce come riferimento ad Object). A partire dal bottone premuto, con il metodo getText() viene restituito il testo visualizzato nel bottone. 2.8 Accedere dall’ascoltatore agli oggetti di una finestra L’approccio finora descritto funziona abbastanza bene anche se presenta ancora alcune problematiche. Il primo problema è legato al fatto che la finestra e i suoi ascoltatori sono divisi in classi separate. Supponiamo di avere ascoltatori per gestire gli eventi scatenati da alcuni componenti installati in una data finestra. Essendo le due classi formalmente indipendenti tra loro, per quanto detto, non è possibile accedere dagli ascoltatori a tutti i componenti della finestra (tranne il componente che ha generato l’evento). Supponiamo di voler realizzare una finestra come quella in Figura 2.14. Alla pressione del bottone OK il contenuto del campo di testo deve essere visualizzato in un message dialog box. Si consideri il codice nel Listato 2.12. Nell’ascoltatore definiamo un nuovo JTextField text. Quest’approccio non funziona perchè il campo di testo definito è un nuovo campo di testo e non quello della finestra. Quindi la pressione del bottone mostrerà sempre 57 2.8 Accedere dall’ascoltatore agli oggetti di una finestra public class MyFrame extends JFrame { private JButton uno = new JButton("Uno"); private JButton due = new JButton("Due"); private JButton tre = new JButton("Tre"); private JButton quattro = new JButton("Quattro"); private JButton cinque = new JButton("Cinque"); Ascoltatore listener = new Ascoltatore(); public MyFrame() { ... Container c = this.getContentPane(); c.add(uno); uno.addActionListener(listener); ... c.add(cinque); cinque.addActionListener(listener); } } public class Ascoltatore implements ActionListener { public void actionPerformed(ActionEvent event) { JButton b = (JButton)event.getSource(); JOptionPane.showMessageDialog(null, "ÈÃstatoÃpremuto"+b.getText()); } } Listato 2.11: Esempio uso di ActionPerformed Figura 2.14: La finestra ottenuta dal Listato 2.12 58 2.8 Accedere dall’ascoltatore agli oggetti di una finestra public class MyFrame extends JFrame { JPanel centro = new JPanel(); JPanel sud = new JPanel(); JTextField txt = new JTextField(20); JButton button = new JButton("Premi"); public MyFrame() { super("Esempio"); centro.add(txt); sud.add(button); getContentPane().add(centro,BorderLayout.CENTER); getContentPane().add(sud,BorderLayout.SOUTH); button.addActionListener(new Listen()); ... } } class Listen implements ActionListener { public void actionPerformed(ActionEvent e) { JTextField text = new JTextField(); JOptionPane.showMessageDialog( null,text.getText()); } } Listato 2.12: Un esempio che non funziona una finestra dal contenuto vuoto perchè di default un campo di testo viene inizializzato con il contenuto vuoto. Un primo approccio è utilizzare una classe interna. Una classe interna è definita all’interno di un’altra classe e può accedere a tutte le variabili e i metodi di istanza della classe esterna (anche a quelli privati). Si consideri il Listato 2.13 che fa uso della classi interne: l’istruzione text = txt nell’ascoltatore fa sı̀ che il riferimento text “punti” allo stesso campo di testo riferito da txt. Quindi quando nel codice successivo, l’ascoltatore chiede text.getText() ottiene effettivamente il codice richiesto. Questa tecnica è accettabile se l’ascoltatore esegue poche operazioni e pochi ascoltatori esistono. Altrimenti la classe MyFrame diventa eccessivamente grande. Inoltre il codice ottenuto diventa poco leggibile perchè si tende ad accorpare classi che rappresentano concetti diversi. La soluzione preferibile è quella in cui l’ascoltatore definisce un costruttore che prende come parametro un riferimento alla finestra con i componenti 59 2.8 Accedere dall’ascoltatore agli oggetti di una finestra public class MyFrame extends JFrame { JPanel centro = new JPanel(); JPanel sud = new JPanel(); JTextField txt = new JTextField(20); JButton button = new JButton("Premi"); public MyFrame() { super("Esempio"); centro.add(txt); ... button.addActionListener(new Listen()); ... } class Listen implements ActionListener { public void actionPerformed(ActionEvent e) { JTextField text = txt; JOptionPane.showMessageDialog( null,text.getText()); } } } Listato 2.13: Una soluzione che funziona con le classi interne 60 2.9 Condividere gli ascoltatori per più oggetti richiesti. Il costruttore memorizza tale riferimento come variabile di classe. La classe ascoltatore resta comunque esterna: essa accede ai widget della finestra tramite tale riferimento e l’operatore punto. Ovviamente i widget non devono essere memorizzati privati. La soluzione è descritta nel Listato 2.14. Nel codice viene anche mostrato una grande potenzialità degli eventi azione: l’evento azione del campo di testo è gestito nello stesso modo della finestra. Ne risulta, di conseguenza, che la pressione del bottone e la pressione del tasto ENTER sul campo di testo vengono gestiti dallo stesso ActionListener e quindi gestiti nello stesso modo. 2.9 Condividere gli ascoltatori per più oggetti La tecnica più naive per gestire più eventi associati ad un controllo è quello di creare una classe “ascoltatore” per ogni oggetto e per ogni classe di eventi da gestire per quello oggetto. Ciò vorrebbe dire che se dovessimo gestire l’evento di pressione di X bottoni, dovrebbe realizzare X classi che implementano ActionListener. Ovviamente è nella pratica improponibile prevedere una classe per ogni bottone, voce del menu e cosı̀ via. Infatti, consideriamo per esempio un’applicazione con 5 finestre, ognuna con 5 bottoni. Supponiamo che una di tali finestre abbia anche 3 voci del menu di 4 opzioni ciascuno. Ciò significherebbe che avremmo 37 classi solo per gestire la pressione dei bottoni o della voci del menu! L’idea più elegante è raggruppare gli oggetti su cui ascoltare in classi, prevedendo un ascoltatore condiviso per tutti gli oggetti della stessa classe. Da questo punto in poi consideriamo solo gli eventi actionListener senza perdere di generalità. Lo stesso approccio si applica in tutti gli altri casi. I componenti della stessa classe, ovviamente, condivideranno lo stesso metodo actionPerformed. A meno che tutti i componenti della stessa classe si devo comportare nello stesso modo allo scatenarsi dell’evento azione, occore essere in grado di “capire” quale oggetto ha generato l’evento. Questo al fine di capire quale comportamente adottare. Esistono due modi per capire “chi” ha generato l’evento: 1. Attraverso il metodo getSource() della classe ActionEvent che restituisce un riferimento all’oggetto che ha scatenato l’evento. In questo modo è possibile utilizzare un approccio di tipo switch/case: se il riferimento “punta” a X, allora chiama un metodo privato che corrisponde alla gestione di pressione del bottone X; se il riferimento “punta” a Y, 61 2.9 Condividere gli ascoltatori per più oggetti public class MyFrame extends JFrame { JPanel centro = new JPanel(); JPanel sud = new JPanel(); JTextField txt = new JTextField(20); JButton button = new JButton("Premi"); Listen ascolt=new Listen(this); public MyFrame() { super("Esempio"); centro.add(txt); ... button.addActionListener(ascolt); txt.addActionListener(ascolt); ... } } class Listen implements ActionListener { MyFrame frame; public Listen(MyFrame aFrame) { frame = aFrame; } public void actionPerformed(ActionEvent e) { JTextField text = frame.txt; JOptionPane.showMessageDialog( null,text.getText()); } } Listato 2.14: La soluzione migliore 62 2.9 Condividere gli ascoltatori per più oggetti allora chiama il metodo per Y. E cosi via. ’E ovviamente chiaro se questa approccio è fattibile se è possibile confrontare i riferimenti ottenuti con il metodo getSource() con i riferimenti memorizzati internamente alla classe che estende JFrame e che disegna la finestra con i bottoni X, Y e gli altri. Questo approccio è generalmente fattibile, quindi, con le classi interne. 2. Utilizzando la proprietà actionCommand, implementata per ogni componente, che permette di associare una stringa identificativa univoca ad ogni componente che scatena un evento azione. A questo punto è possibile definire all’interno del metodo actionPerformed un approccio di tipo switch/case sugli actionCommand. Se lo actionCommand dell’oggetto è “ACTX” (che è stato associato ad X) allora l’ascoltatore esegue una certa porzione di codice, se lo actionCommand è “ACTY” fa un’altra cosa. E cosı̀ via. In questo nuovo contesto non è più necessario per l’ascoltatore di avere accesso ai riferimenti. Si consideri, ad esempio, la porzione di condice nel Listato 2.15 dove ci sono tre voci di menu UpOpt, DownOpt, RandomOpt. Le tre voci del menu hanno associati lo stesso ascoltatore con lo stesso metodo actionPerformed. All’interno del metodo la discriminazione su come l’ascoltatore si deve comportare in funzione del metodo invocato è fatto analizzando i riferimenti. L’istruzione src=e.getSource() restituisce un riferimento all’oggetto che ha generato l’evento. Se tale riferimento è uguale a UpOpt (entrambi puntano allo stesso oggetto corrispondente alla voce del menu “Up”, allora la voce del menu scelta è Up. Quindi viene eseguita la porzione di codice relativa a tale voce. Lo stesso per le altre voci. L’ascoltatore è realizzato come classe interna in modo tale da poter realizzare l’uguaglianza tra i puntato e verificare se coincidono. Come si è detto, l’approccio con le classi interne ha molti svantaggi. la metodologia che vogliamo qui introdurre per capire “chi” ha generato l’evento non deve assumere classi interne e, quindi, non può basarsi sull’uguaglianza tra puntatori. Quando si usano classi esterne, è possibile “capire” il componente che ha notificato l’evento associando ai diversi componenti un diverso valore della proprietà actionCommand. Nell’esempio nel Listato 2.16 i possibili valori per le proprietà actionCommand sono memorizzate come costanti stringa, cioè public final static, della classe ascoltatore Listener. Ad ogni oggetto da predisporre per gestire gli eventi azione viene associato un valore per la actionCommand presi tra tali costanti stringa. Questo viene fatto grazie al metodo 63 2.9 Condividere gli ascoltatori per più oggetti public class MyFrame extends JFrame { ... JMenuItem UpOpt = new JMenuItem("Up"); JMenuItem DownOpt = new JMenuItem("Down"); JMenuItem RandomOpt = new JMenuItem("Random"); Listener ascoltatore = new Listener(); public MyFrame() { ... UpOpt.addActionListener(ascoltatore); DownOpt.addActionListener(ascoltatore); RandomOpt.addActionListener(ascoltatore); ... } class Listener implements ActionListener { public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == UpOpt) { codice della voce del menu Up } else if (src == DownOpt) { codice della voce del menu Down } else if (src == RandomOpt) { codice della voce del menu Random } } } } Listato 2.15: Ascoltatore condiviso come classe Interna 64 2.9 Condividere gli ascoltatori per più oggetti public class MyFrame extends JFrame { ... JMenuItem UpOpt = new JMenuItem("Up"); JMenuItem DownOpt = new JMenuItem("Down"); JMenuItem RandomOpt = new JMenuItem("Random"); Listener ascolt = new Listener(); public MyFrame() { ... UpOpt.addActionListener(ascolt); UpOpt.setActionCommand(ascolt.UPOPT); DownOpt.addActionListener(ascolt); DownOpt.setActionCommand(ascolt.DOWNOPT); RandomOpt.addActionListener(ascolt); RandomOpt.setActionCommand(ascolt.RANDOMOPT) ... } } Listato 2.16: Il JFrame per usare ascoltatori esterni void setActionCommand(String); Il metodo actionPerformed dell’actionListener legge il valore della proprietà actionCommand del componente che ha notificato l’evento e, in funzione del valore letto, sceglie la porzione di codice da eseguire. Eventualmente è possibile associare lo stesso actionCommand a componenti gestiti dallo stesso ascoltatore se si desidera che questi gestiscano l’evento azione nello stesso modo. Questo cosa è molto utile quando si desidera replicare la voce di un dato menu con un bottone da inserire nella toolbar della finestra: per fare sı̀ che sia la voce del menu che il bottone nella toolbar gestiscano l’evento nello stesso modo è sufficiente associare ad entrambi lo stesso actionCommand. Concludiamo il capitolo con l’ascoltatore relativo all’esempio nel Listato 2.16. Il suo scheletro è nel Listato 2.17. Si possono osservare sempre tre parti in questo tipo di ascoltatori: • La parte che definisce le costanti stringa (le tre dichiarazioni di oggetti pubblici, costanti3 e statici) 3 In Java è possibile definire che il valore di un riferimento non può cambiare dichiarandolo final. Questo ovviamente non è sufficiente per dichiarare un oggetto costante. Tuttavia nel caso di oggetti String le due cose coincidono dato che gli oggetti stringa sono immutabili: non può cambiare il riferimento, nè può cambiare il valore dell’oggetto puntato 65 2.9 Condividere gli ascoltatori per più oggetti public class Listener implements ActionListener { public final static String UPOPT = "up"; public final static String DOWNOPT = "down"; public final static String RANDOMOPT = "random"; public void actionPerformed(ActionEvent e) { String com = e.getActionCommand(); if (com == UPOPT) upOpt(); else if (src == DOWNOPT) downOpt(); else if (src == RANDOMOPT) randomOpt(); } private void upOpt() { ... } private void randomOpt() { ... } private void downOpt() { ... } } Listato 2.17: Ascoltatore condiviso come classe Interna • Il metodo actionPerformed che gestisce l’evento. Questo metodo in questi casi non fa nient’altro che leggere il valore della proprietà actionCommand dell’oggetto che ha generato l’evento. In funzione del valore della proprietà (e quindi dell’oggetto scatenante), viene attivata una gestione separata che consiste nell’invocare metodi diversi. • I diversi metodi privati che eseguono effettivamente le operazioni associate all’attivazione dei diversi controlli/widget. 66 2.10 Conclusioni 2.10 Conclusioni Questo capitolo ha introdotto la progettazione delle Graphical User Interface (GUI) attraverso il package Swing. Swing garantisce alta portabilità tra le piattaforme grazie al fatto che la JVM stessa disegna le linee e colora per creare i controlli. Questo è anche uno degli svantaggi delle Swing. Disegnare e colorare a livello di JVM è fatto parzialmente in “modo utente” del Sistema Operativo. Quindi, risulta meno efficiente e veloce rispetto a delegare al Sistema operativo con delle chiamate di sistema che vengono eseguite in “modo superuser”. Questo aspetto, per fortuna, in molti casi va in secondo piano grazie al fatto che le moderne schede video, progettate per videogiochi, risultano molto potenti. 67 Capitolo 3 Servlet e Java Server Pages (JSP) 3.1 Introduzione alle Servlet Uno dei settori sui quali si focalizza maggiormente l’attenzione dello scenario della Information Technology è quello della programmazione “web oriented”, ovvero quella in cui la parte client è costituita da un semplice browser che interagisce con la parte server per mezzo del protocollo HTTP. Questa tipologia di programmi, il cui modello di funzionamento viene tipicamente denominato Common Gateway Interface (CGI), ha l’obiettivo di permettere l’interfacciamento da parte di un client web con una serie di risorse e di servizi residenti sul server. Poco prima dell’uscita definitiva del JDK 1.2, per la realizzazione di applicazioni CGI in Java, Sun ha introdotto la Servlet API, diventata in poco tempo una delle più importanti di tutta la piattaforma Java2. Le servlet hanno introdotto alcune importanti innovazioni nel modello operativo del CGI, innovazioni che poi sono state adottate di riflesso sia nelle tecnologie più obsolete, sia in quelle direttamente concorrenti. Secondo la definizione ufficiale, una servlet è un “componente lato server per l’estensione di web server Java enabled”, cioè un qualcosa di più generale del solo WWW. Una servlet è quindi un programma Java in esecuzione sul server e in grado di colloquiare con il client per mezzo del protocollo HTTP. Tipicamente questo si traduce nella possibilità di generare dinamicamente contenuti web da visualizzare nella finestra del client-browser. I package che contengono tutte le classi necessarie per la programmazione delle servlet sono il javax.servlet e il javax.servlet.http. Quando si lavora con le servlet, è importante specificare quale versione 68 3.2 Il Ciclo di Vita di una Servlet della API si utilizza, dato che il servlet engine utilizzato potrebbe non supportare l’ultima versione delle API. Con la versione 2.2, le Servlet API sono entrate a far parte formalmente della cosiddetta Java 2 Enterprise Edition, al fianco di altre importanti tecnologie come JDBC, JNDI, JSP, EJB e RMI. Questa nuova organizzazione logica, anche se non ha alterato l’architettura di base, ha introdotto alcune nuove definizioni e formalismi. L’innovazione più importante da questo punto di vista è l’introduzione del concetto di “container” al posto di server: un container è un oggetto all’interno del quale una servlet vive e nel quale trova un contesto di esecuzione personalizzato. Il concetto di container ha poi una visione più ampia dato che viene adottato anche nel caso di JSP ed EJB. Essendo composta al 100% da classi Java, una servlet è integrabile con il resto della tecnologia Java dando luogo a soluzioni scalabili in maniera molto semplice. Ogni volta che il client esegue una richiesta di un servizio basato su CGI, il web server deve mandare in esecuzione un processo dedicato per quel client. Se n client effettuano la stessa richiesta, allora n processi devono essere prima istanziati e poi eseguiti. Questo comporta un notevole dispendio di tempo e di risorse di macchina. Il modello multithread permette a una sola servlet, dopo la fase di inizializzazione, di servire un numero n di richieste senza la necessità di ulteriori esecuzioni. Tutto questo porta a notevoli vantaggi dal punto di vista della efficienza e delle potenzialità operative. Va sottolineato il fatto che utilizzare una applicazione web basata su servlet, pagine JSP o comunemente Java, garantisce portabilità dell’applicazione stessa, in quanto essa potrà essere spostata da una piattaforma a un’altra in funzione delle esigenze e del carico di lavoro: tutti i sistemi operativi moderni, infatti, hanno una JVM di ultima generazione. 3.2 Il Ciclo di Vita di una Servlet Quando una servlet è invocata per la prima volta, il servlet container (Tomcat) chiama il metodo init(). Tale metodo serve per inizializzare la servlet e per il corretto funzionamento successivo, ed è qui che tipicamente si eseguono le operazioni computazionalmente costose da effettuare una volta per tutte (come ad esempio la connessione a un DB). Dal punto di vista dell’implementazione, per realizzare una servlet HTTP è sufficiente estendere la classe HttpServlet, appartenente al package javax.servlet.http, e ridefinire alcuni metodi fondamentali, uno per personalizzare l’inizializzazione della servlet (init(), appunto!) e altri per definirne invece il comportamento in 69 3.2 Il Ciclo di Vita di una Servlet funzione delle invocazioni del client. In Figura 3.1 è mostrata la gerarchia delle principali classi per la gestione delle servlet. Il metodo init(), derivato Figura 3.1: Gerarchia delle classi principali per la gestione delle Servlet dalla classe GenericServlet, ha la seguente firma: public void init() throws ServletException Se durante il processo di inizializzazione si verifica un errore tale da compromettere il corretto funzionamento successivo, allora la servlet potrà segnalare tale evento generando una eccezione di tipo ServletException. In questo caso, la servlet non verrà resa disponibile per l’invocazione e l’istanza appena creata verrà immediatamente rilasciata. Dopo il rilascio dell’istanza fallita, il server procederà immediatamente, alla istanziazione e inizializzazione di una nuova servlet; la generazione di una UnavailableException permette di specificare il tempo minimo necessario da attendere prima di intraprendere nuovamente il processo di inizializzazione. Dopo l’inizializzazione, una servlet si mette in attesa di una eventuale chiamata da parte del client, che potrà essere indistintamente una GET o una POST HTTP. La differenza fondamentale tra questi due tipi di chiamata è che con una chiamata di tipo GET i parametri sono passati nella URL della richiesta, separati da una “&”, ad esempio: 70 3.2 Il Ciclo di Vita di una Servlet www.unsito.it\MyServer?a=4\&b=2 Con una chiamata di tipo POST invece i parametri vengono specificati nello header del pacchetto HTTP. Quindi, poichè una URL è composta al più da 255 byte, c’è un limite al numero di parametri e alla loro lunghezza quando si usa una GET. Un vantaggio invece nell’utilizzo di questo tipo di chiamata sta nel fatto che trattandosi di una URL, una chiamata di tipo GET potrà essere memorizzata dal browser come un bookmark (tra i preferiti). L’interfaccia Servlet mette a disposizione il metodo service() che viene invocato direttamente dal server in modalità multithread (il server, per ogni invocazione del client, manda in esecuzione un metodo service in un thread separato). In questo modo, il programmatore dovrà solo preoccuparsi di scrivere il codice relativo alle operazioni da effettuare in funzione della richiesta di un client. La firma del metodo è la seguente: public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException Il service viene invocato indistintamente sia nel caso di una invocazione tipo GET che di una POST. La ridefinizione del metodo service permette di definire il comportamento della servlet stessa. Se nella servlet è definito il metodo service(), esso è eseguito al posto dei metodi doGet() e doPost(). I due parametri passati sono ServletRequest e ServletResponse, che permettono di interagire con la richiesta effettuata dal client e di inviare risposta tramite pacchetti HTTP. In seguito saranno analizzati in dettaglio gli aspetti legati a queste due interfacce. Nel caso in cui invece si desideri implementare un controllo più fine si possono utilizzare i due metodi doGet() e doPost(), che risponderanno rispettivamente a una chiamata di tipo GET e a una di tipo POST. Ad esempio, se si vuole evitare che una servlet sia invocata con una POST, si può implementare soltanto il metodo doGet(); oppure, nel caso in cui si voglia gestire in modo differente i due tipi di chiamata, basta implementare in maniera appropriata i due metodi corrispondenti. A completamento del ciclo di vita, vi è la fase di “distruzione” della servlet. Il metodo che viene utilizzato per questo scopo è destroy(), che consente altresı̀ la terminazione del processo e il log dello status. La ridefinizione di tale metodo, derivato dalla interfaccia Servlet, permette di specificare tutte le operazioni simmetriche alla inizializzazione, oltre a sincronizzare e a rendere persistente lo stati della memoria. Anche la chiamata della destroy() come quella della init() è a carico del server che ha in gestione la servlet. La distruzione di una servlet e il relativo scaricamento dalla memoria avvengono solamente nel momento in cui tutte le chiamate a service() da 71 3.3 Come Interagire con una Servlet: Richieste e Risposte 72 parte dei client sono state eseguite. Quando invece si verifica un errore durante il processo di inizializzazione e la servlet non può essere resa disponibile, l’istanza appena creata verrà immediatamente rilasciata, ma il metodo destroy() in tale situazione non verrà invocato dal server. 3.3 Come Interagire con una Servlet: Richieste e Risposte Una servlet può comunicare con il client in maniera bidirezionale per mezzo delle due interfacce HttpServletRequest e HttpServletResponse: la prima rappresenta la richiesta e contiene i dati provenienti dal client, mentre la seconda rappresenta la risposta e permette di incapsulare tutto ciò che deve essere inviato indietro al client stesso. La Risposta: HttpServletResponse La risposta di una servlet può essere inviata al client per mezzo di un oggetto di tipo HttpServletResponse, che offre due metodi per inviare dati al client: il getWriter() che restituisce un oggetto di tipo java.io.Writer, e il getOutputStream() che, come lascia intuire il nome, restituisce un oggetto di tipo ServletOutputStream (Figura 3.2). Si deve utilizzare il primo tipo per inviare dati di tipo testuale al client, mentre un OutputStream può essere utile per inviare anche dati in forma binaria. Da notare che la chiusura di un Writer o di un ServletOutputStream dopo l’invio dei dati permette al server di conoscere quando la risposta è completa. Con i metodi setContentType() e setHeader() si può stabilire il tipo MIME della pagina di risposta e, nel caso sia HTML, avere un controllo fine su alcuni parametri. Ad esempio, il content-type può essere impostato a “text/html”, a indicare l’invio di una pagina HTML al browser, mentre “pragma no-cache” serve per forzare il browser a non memorizzare la pagina dalla cache. 3.3.1 Esempio Data Vediamo un semplice esempio, in cui la servlet deve restituire una pagina HTML in output con la data e l’ora corrente (che ovviamente cambierà ad ogni invocazione!) e in più la data e l’ora in cui è stato invocato il metodo init() (che sarà la stessa per ogni invocazione!). Per l’installazione e le operazioni preliminari da effettuare per poter utilizzare JBOSS, fare riferimento all’Appendice A.1. I Listati 3.1 e 3.2 contengono il codice della classe GeneraDataInit. 3.3 Come Interagire con una Servlet: Richieste e Risposte import import import import javax.servlet.http.*; javax.servlet.*; java.io.*; java.util.*; public class GeneraDataInit extends HttpServlet { Calendar dataInit; private String convertToString (Calendar data) { String giornoSettimana = null; String ilMese = null; String giornoMese, lAnno, lOra; String iMinuti, iSecondi, dataFinale; int ggSettimana = data.get(Calendar.DAY_OF_WEEK); switch (ggSettimana) { case 1: giornoSettimana = "domenica,Ã"; break; case 2: giornoSettimana = "lunedi’,Ã"; break; case 3: giornoSettimana = "martedi’,Ã"; break; case 4: giornoSettimana = "mercoledi’,Ã"; break; case 5: giornoSettimana = "giovedi’,Ã"; break; case 6: giornoSettimana = "venerdi’,Ã"; break; case 7: giornoSettimana = "sabato,Ã"; break; } int ggMese = data.get(Calendar.DAY_OF_MONTH); giornoMese = ggMese+"Ã"; int mese = data.get(Calendar.MONTH); switch (mese) { case 0: ilMese = "GennaioÃ"; break; case 1: ilMese = "FebbraioÃ"; break; case 2: ilMese = "MarzoÃ"; break; case 3: ilMese = "AprileÃ"; break; case 4: ilMese = "MaggioÃ"; break; case 5: ilMese = "GiugnoÃ"; break; case 6: ilMese = "LuglioÃ"; break; case 7: ilMese = "AgostoÃ"; break; case 8: ilMese = "SettembreÃ"; break; case 9: ilMese = "OttobreÃ"; break; case 10: ilMese = "NovembreÃ"; break; case 11: ilMese = "DicembreÃ"; break; } Listato 3.1: Classe GeneraDataInit 73 3.3 Come Interagire con una Servlet: Richieste e Risposte int anno = data.get(Calendar.YEAR); lAnno = anno+",Ã"; int ore = data.get(Calendar.HOUR_OF_DAY); lOra = ore+":"; int minuti = data.get(Calendar.MINUTE); if(minuti<10) iMinuti = "0"+minuti+":"; else iMinuti = minuti+":"; int secondi = data.get(Calendar.SECOND); if(secondi<10) iSecondi = "0"+secondi+".Ã"; else iSecondi = secondi+".Ã"; dataFinale = giornoSettimana+giornoMese +ilMese+lAnno+lOra+iMinuti+iSecondi; return dataFinale; } public void init() throws ServletException { dataInit = Calendar.getInstance(); } public void service (HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { OutputStream os = res.getOutputStream(); PrintWriter out = new PrintWriter(os, true); out.println ("<HTML><HEAD><TITLE>GeneraDataMigliore </TITLE></HEAD>"); out.println ("<BODY>"); out.println ("Ciao,ÃlaÃdataÃeÃl’oraÃcorrentiÃsono:Ã"); Calendar rightNow = Calendar.getInstance(); out.println (convertToString(rightNow)); out.println ("<BR>"); out.println ("LaÃdataÃeÃl’oraÃinÃcuiÃe’ÃstatoÃinvocato il metodo init sono le seguenti: "); out.println (convertToString(dataInit)); out.println ("</BODY>"); out.println ("</HTML>"); } } Listato 3.2: Classe GeneraDataInit (continuo) 74 3.3 Come Interagire con una Servlet: Richieste e Risposte Figura 3.2: Classi per la gestione delle risposte verso il client: ServletResponse e HTTPServletResponse Il file web.xml relativo all’esempio è nel Listato 3.3. Si supponga di impacchettare l’intero esempio in un archivio datainit.war. Per invocare la servlet sarà necessario scrivere nella barra degli indirizzi del browser http://localhost:8080/datainit/Init La Richiesta: HttpServletRequest La HttpServletRequest, oltre a permettere l’accesso a tutte le informazioni relative all’header HTTP (come ad esempio i vari cookies memorizzati nella cache del browser), permette di ricavare i parametri passati insieme all’invocazione del client. Tali parametri sono inviati come coppie nome-valore, sia che la richiesta sia di tipo GET che POST. Ogni parametro può assumere valori multipli. L’interfaccia ServletRequest dalla quale discende la HttpServletRequest mette a disposizione alcuni metodi per ottenere i valori dei parametri passati alla servlet: ad esempio, per ottenere il valore di un parametro dato il nome, si può utilizzare il metodo fornito dalla interfaccia public String getParameter(String name) 75 3.3 Come Interagire con una Servlet: Richieste e Risposte <?xml version="1.0" encoding="UTF-8"?> <!-- per invocarla http://localhost:8080/datainit/Init --> <servlet> <servlet-name>servletDataInit</servlet-name> <servlet-class>GeneraDataInit</servlet-class> </servlet> <servlet-mapping> <servlet-name>servletDataInit</servlet-name> <url-pattern>/Init</url-pattern> </servlet-mapping> </web-app> Listato 3.3: File web.xml Nel caso di valori multipli per la stessa variabile - come quando l’invocazione viene fatta tramite un form HTML - a partire dalla API 2.1 si ottiene sempre un array di stringhe, risultato analogo a quello fornito dal metodo public String[] getParameterValues(String name) Per ottenere invece tutti i nomi o tutti i valori dei parametri si possono utilizzare i metodi public Enumeration getParameterNames() public String[] getParameterValues(String name) Infine, nel caso di una GET http, il metodo getQueryString() restituisce una stringa con tutti i parametri concatenati. Nel caso in cui si attendano dati non strutturati, in forma testuale, e l’invocazione sia una POST, una PUT o una DELETE, si può utilizzare il metodo getReader(), che restituisce un BufferedReader. Se invece i dati inviati sono in formato binario, allora è indicato utilizzare il metodo getInputStream(), il quale a sua volta restituisce un oggetto di tipo ServletInputStream. Ecco di seguito un semplice esempio public class MyServlet extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException { res.setContentType("text/html"); res.setHeader("Pragma", "no-cache"); res.writeHeaders(); String param = req.getParameter("parametro"); 76 3.3 Come Interagire con una Servlet: Richieste e Risposte // esegue programma di servizio OutputStream out = res.getOutputStream(); // usa out per scrivere sulla pagina di risposta } } Da notare l’utilizzo dello statement OutputStream out = res.getOutputStream(); impiegato per ricavare lo stream di output con il quale inviare dati (ad esempio codice HTML) al client. 3.3.2 Esempio Rubrica In questo esempio vediamo come, a partire da una form HTML, possiamo richiedere a un server il numero di telefono corrispondete al nome della persona specificata (attraverso la form, appunto). La form è quella in Figura 3.3 e il codice HTML relativo è nel Listato 3.4. La pressione del bottone di submit della form deve invocare una servlet codificata nella classe Rubrica (vedi Listati 3.6 e 3.7). L’ultimo punto da analizzare è come configurare l’applicazione in modo tale che la pressione del bottone di submit invochi direttamente la servlet rubrica. Analizzando il file web.xml(Listato 3.8) si osserva che la servlet è invocabile mediante l’URL http://localhost:8080/nome war/rubrica. Ciò vuol dire che l’elemento form della pagina HTML dovrà specificare come action rubrica, ovvero l’URL relativo della servlet. Si osservi inoltre il nome dei campi di input della form: il campo di testo si chiama “Nome” e quindi alla servlet Rubrica verrà passato un parametro Nome il cui valore è il contenuto del campo di testo. Inoltre esistono tre checkbox. Il comportamento delle checkbox nel passaggio di parametri alla servlet è leggermente diverso: solo se una checkbox è selezionata allora viene passato come parametro il nome della checkbox (e valore quello specificato nell’attributo value). Nel caso specifico, ad esempio, se la checkbox relativa al numero di casa è selezionata, allora alla servlet verrà passato il parametro Casa con valore “ON”. Se la stessa non è selezionata, il parametro non verrà passato e quindi l’invocazione dell’istruzione getParameter(‘‘Casa’’) restituirà null. La classe fa uso dei servigi della classe Numero descritta nel Listato 3.5 che permette l’accesso al repository dei numeri di telefoni. Si osservi come l’unico costruttore della classe sia privato a sottolineare che gli oggetti di tale classe possono essere creati solamente attraverso il metodo static ottieniNumero(String abbonato). Per semplicità il repository è implementato come un file di testo Rubrica.txt una cui instanza è rappresentata in Figura 3.3. 77 3.3 Come Interagire con una Servlet: Richieste e Risposte <html> <head> <title>Nome</title> </head> <body> <form method="GET" action="rubrica"> <p>Nome: <input type="text" name="Nome" size="20"> </p> <p>Tipo: <input type="checkbox" name="Casa" value="ON"> Casa <input type="checkbox" name="Cellulare" value="ON"> Cellulare <input type="checkbox" name="Ufficio" value="ON"> Ufficio </p> <p><input type="submit" value="Invia"> <input type="reset" value="Reimposta"></p> </form> </body> </html> Listato 3.4: Il file HTML della form 78 3.3 Come Interagire con una Servlet: Richieste e Risposte import java.io.*; public class Numero { public static String nome; public String casa; public String ufficio; public String cellulare; private Numero() {} public static Numero ottieniNumeri (String unNome) throws IOException { FileReader in = new FileReader ("C:\\Rubrica.txt"); BufferedReader riga = new BufferedReader (in); nome = riga.readLine(); while(!nome.equals(unNome) && nome!=null) { riga.readLine(); riga.readLine(); riga.readLine(); nome = riga.readLine(); } if (nome.equals(unNome)) { Numero n = new Numero(); n.casa = riga.readLine(); n.ufficio = riga.readLine(); n.cell = riga.readLine(); return (n); } else return null; } } Listato 3.5: Classe Numero 79 3.3 Come Interagire con una Servlet: Richieste e Risposte import javax.servlet.http.*; import javax.servlet.*; import java.io.*; public class Rubrica extends HttpServlet { public void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException { String nome=req.getParameter("Nome"); boolean casa=(req.getParameter("Casa")==null); boolean ufficio=(req.getParameter("Ufficio")==null); boolean cell=(req.getParameter("Cellulare")==null); OutputStream os = resp.getOutputStream(); PrintWriter out = new PrintWriter(os, true); out.println ("<HTML><HEAD><TITLE>Esempio2</TITLE> </HEAD>"); out.println ("<BODY>"); out.println ("Ciao,ÃsonoÃstatiÃrichiestiÃi seguenti numeri di telefono di "); out.println ("<B>"); out.println (nome); out.println ("</B>"); out.println (":"); out.println ("<BR>"); Numero unNumero = Numero.ottieniNumeri(nome); Listato 3.6: Classe Rubrica 80 3.3 Come Interagire con una Servlet: Richieste e Risposte if(!casa) { out.println ("<P>"); out.println ("<I>"); out.println ("Casa:"); out.println ("</I>"); out.println (unNumero.casa); out.println ("</P>"); } if(!ufficio) { out.println ("<P>"); out.println ("<I>"); out.println ("Ufficio:"); out.println ("</I>"); out.println (unNumero.ufficio); out.println ("</P>"); } if(!cell) { out.println ("<P>"); out.println ("<I>"); out.println ("Cellulare:"); out.println ("</I>"); out.println (unNumero.cell); out.println ("</P>"); } out.println ("</BODY>"); out.println ("</HTML>"); } } Listato 3.7: Classe Rubrica 81 3.4 Le Sessioni 82 Figura 3.3: La Form HTML I numeri di telefono vanno memorizzati su un documento di testo, come mostrato in Figura 3.4. 3.4 Le Sessioni Il fatto che più esecuzioni di una stessa servlet facciano capo allo stesso oggetto permette di condividere proprietà e metodi della classe e, in definitiva, l’informazione. In pratica ciò si traduce nell’istanziare nel metodo init delle variabili di classe, che saranno successivamente visibili nelle varie esecuzioni dei metodi doPost e doGet (o service) della nostra servlet. La necessità di passare informazioni tra le pagine Web è particolarmente sentita tra chi sviluppa procedure Intranet / Extranet, nelle quali, una volta identificato l’utente, si permette che esso compia una serie di operazioni che non si concludono necessariamente in una pagina; è cosı̀ necessario tenere traccia delle operazioni, fornire un ambiente che si adatti all’utente stesso secondo le sue preferenze e diritti di accesso; questo si traduce tecnicamente nel mantenimento dell’indentità dell’utente e dello stato attraverso la richiesta di più pagine. Viene cosı̀ ad identificarsi un concetto analogo a quanto noto da anni nei mainframe e nei DBMS, cioè la sessione. L’uso di variabili 3.4 Le Sessioni 83 Figura 3.4: Rubrica.txt <?xml version="1.0" encoding="UTF-8"?> <servlet> <servlet-name>servletRubrica</servlet-name> <servlet-class>Rubrica</servlet-class> </servlet> <servlet-mapping> <servlet-name>servletRubrica</servlet-name> <url-pattern>/rubrica</url-pattern> </servlet-mapping> </web-app> Listato 3.8: File web.xml di classe in questa ottica non è più sufficiente, in quanto esse sono visibili da qualunque client invochi quella servlet, noi invece vogliamo tenere traccia di una informazione specifica per quella servlet chiamata da quel particolare client. Le Servlet API forniscono una serie di strumenti per fare tutto ciò in maniera efficace e rapida, dunque in pieno stile Java. Cosa accade quando si crea una nuova sessione? Il server deve predisporre l’area della memoria che conterrà l’informazione e fare in modo di “marcare” il browser dell’utente in modo da poter successivamente associare ogni richiesta di quel client con quella determinata sessione; in pratica viene registrato un cookie sul browser con un numero di identificazione della sessione, il Session Id. Molte persone si preoccupano per i cookie e per la scarsa sicurezza che offrono in quanto l’utente un po’ smaliziato può divertirsi a modificarli 3.4 Le Sessioni e creare problemi all’applicazione; nel caso delle sessioni in Java ciò non è possibile in quanto l’informazione continua a risiedere là dove è più al sicuro, cioè sul server. Per gestire le sessioni in Java, il package javax.servlet.http mette a disposizione tre classi: HttpSession, HttpSessionBindingListener e HttpSessionContext. Una volta inizializzata correttamente la servlet, sarà possibile ottenere nel metodo doGet, doPost o service un riferimento all’oggetto HttpSession tramite una chiamata al metodo getSession di HttpServletRequest: HttpSession session = request.getSession(); La servlet cerca di identificare il client (nel nostro caso il browser) che la chiama; se esiste già un oggetto HttpSession per quel client session, allora riferirà a quello, altrimenti ne verrà creato uno nuovo. Riassumendo, ecco cosa accade: 1. Il browser richiede la servlet (ad es.: localhost/servlet/MyServlet) 2. La servlet controlla se il browser richiedente possiede un cookie, se lo possiede verifica se esso punta ad una sessione valida. Altrimenti crea una nuova sessione e ne registra l’identificativo in un cookie (generato da una Servlet che gira col web server) 3. Una volta ottenuto l’oggetto session è possibile usarlo per memorizzare qualsiasi tipo di informazione, basta usare gli opportuni metodi di HttpSession. Ad esempio, per tenere un semplice contatore delle volte che viene chiamata quella servlet da quel determinato client, sarà sufficiente: // Tento di ricavare dalla sessione // il valore di sessiontest.counter Integer ival = (Integer) session.getValue("sessiontest.counter"); if (ival==null) // ival nonè definito... ival = new Integer(1); else // il valore di ivalè definito, lo // incremento di uno ival = new Integer(ival.intValue() + 1); // Memorizzo nella sessione il nuovo valore session.putValue("sessiontest.counter", ival); 84 3.4 Le Sessioni Questo esempio è tratto dal tutorial del JSDK 2.0, per il codice completo potete fare riferimento a quello [3]. Una sessione rimane valida finché non viene invalidata esplicitamente mediante il metodo invalidate, oppure supera un determinato tempo di inattività. Il time-out può essere settato a proprio piacimento, attraverso il metodo setMaxInactiveInterval. Le sessioni sono un meccanismo intrinseco del web server; ciò significa anche che non tutti i Web Server che permettono di usare servlet permettono anche di usare le sessioni, inoltre si potrebbero sperimentare alcune differenze di comportamento tra un Web Server e l’altro. Il cookie presenta numerosi vantaggi: è molto elegante perché il tutto avviene di nascosto, senza annoiare l’utente con URL complesse, inoltre, anche se il browser viene chiuso, il cookie viene mantenuto in modo da poter riprendere il lavoro da dove l’avevamo lasciato. Se però in una logica Intranet è abbastanza plausibile aspettarsi che tutti i browser supportino i cookie o comunque siano configurati per usarli, una volta nel mare di Internet questa certezza non c’è più; in effetti molti utenti preferiscono disabilitare i cookie per motivi di sicurezza. Un aspetto particolarmente interessante delle HttpSession è che queste si preoccupano per noi di verificare se sono uitilizzabili i cookie sul client (browser), e di eseguire le opportune contromosse in caso negativo. In pratica, invece di usare i cookie, viene aggiunto in coda alla query string il Session ID, cosicché l’URL prende questo forma: http://localhost/servlet/MyServlet?jrunsessionid =912773795120311538 L’unica accortezza che dobbiamo usare nello scrivere il codice è di generare le URL attraverso il metodo HttpServletResponse.encodeUrl, in modo che venga inserita nella pagina HTML la chiamata opportuna per entrambe le situazioni. 3.4.1 Esempio Login Il seguente esempio mostra l’uso delle sessioni per realizzare il login. Il client inizia invocando la servlet codificata nella classe HomeServlet (Listato 3.9) che è accedibile dal client inserendo nella barra degli indirizzi del browser l’indirizzo http://localhost:8080/sessioni/start (ovviamente supponendo che il deploy sia stato fatto mediante il file sessioni.war). Tale servlet legge dalla sessione l’attributo “login” che, nell’esempio, è predisposto per memorizzare l’account dell’utente che si è loggato. Se non esiste nella tabella delle sessioni nessun valore per quello attributo, allora significa che l’utente non si è ancora loggato. In tal caso la servlet ridirezione il browser dell’utente sulla 85 3.4 Le Sessioni 86 import javax.servlet.http.*; import javax.servlet.*; import java.io.*; public class HomeServlet extends HttpServlet { public void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession s = req.getSession(true); Object value = s.getAttribute("login"); if(value==null) { resp.sendRedirect("login.htm"); } else resp.sendRedirect("homepage.htm"); } } Listato 3.9: Classe HomeServlet pagina login, altrimenti il browser viene ridirezionato alla pagina principale. La ridirezione viene fatta con il metodo HttpServletResponse.sendRedict(String url). La Figura 3.5 contiene la pagina del Login con un unico campo di testo il cui nome è Nome. La pressione del bottone di Submit invoca la servlet LoginServlet, passando ovviamente come parametro il nome dell’utente che desidera loggarsi. La servlet è molto semplice: prende il parametro Nome ed aggiunge l’attributo “login” alla tabella delle sessioni. Quindi effettua la redirezione alla homepage. La successiva volta in cui l’utente invoca la servlet all’indirizzo http://localhost:8080/sessioni/start, l’output verrà automaticamente redirezionato alla pagina homepage. Questo perchè troverà nella tabella delle sessioni un valore impostato per l’attributo “login”. 3.4 Le Sessioni 87 Figura 3.5: La pagina per il login import javax.servlet.http.*; import javax.servlet.*; import java.io.*; public class LoginServlet extends HttpServlet { public void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String nome = req.getParameter("Nome"); HttpSession s = req.getSession(true); s.setAttribute("login", nome); resp.sendRedirect("homepage.htm"); } } Listato 3.10: Classe LoginServlet 3.5 Le Java Server Pages (JSP) <?xml version="1.0" encoding="UTF-8"?> <servlet> <servlet-name>homesessioni</servlet-name> <servlet-class>HomeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>homesessioni</servlet-name> <url-pattern>/start</url-pattern> </servlet-mapping> <servlet> <servlet-name>loginsessioni</servlet-name> <servlet-class>LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>loginsessioni</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> </web-app> Listato 3.11: File web.xml 3.5 Le Java Server Pages (JSP) Per venire in contro all’esigenza di una maggiore interazione tra client e server e per fornire all’utente contenuti più vari e meno statici sono state introdotte svariate tecnologie, che introducono elementi dinamici nei siti web. Il supporto di queste tecnologie ha comportato modifiche ai vari elementi coinvolti nell’interazione client/server: estensione delle funzionalità del client (web browser), estensione delle funzionalità del server ed estensione del linguaggio HTML. Nuove versioni dei browser e dei server si sono unite alla realizzazione di moduli aggiuntivi, detti plugin, che vengono collegati, tramite opportune API, con il browser o con il server web. É possibile stilare una classificazione sulla base di dove viene eseguito il codice che compie le elaborazioni che rendono l funzionamento dinamico. Tecnologie lato client (client side) la dinamicità e l’interazione con l’utente sono gestite direttamente da codice eseguito dal client o da un suo plugin. É il caso delle Applet Java. Tecnologie lato server (server side) il client ha un ruolo passivo ed è il server, o un suo plugin, a gestire a parte dinamica. Il caso più tipico 88 3.5 Le Java Server Pages (JSP) è la comunicazione client/server per mezzo del protocollo HTTP, supportata in HTML dai form e dai suoi componenti. Attraverso questi, l’utente può inserire dati, mandarli al server e ottenerne una risposta. Il browser manda questi dati sotto forma di una richiesta HTTP, utilizzando uno dei metodi previsti dal protocollo (in genere get o post) a cui il server fa seguire una appropriata risposta. Queste tecnologie devono innanzitutto fornire al programmatore i mezzi per scrivere sul lato server il codice che elabora le risposte: in Java, troviamo i Servlet e le Java Server Pages. Approcci misti presentano elementi dinamici sia sul lato server che sul lato client, realizzati mediante l’interazione dei rispettivi plugin (lato server e lato client). Rientra in questa categoria l’uso congiunto di Applet e Servlet Java. Tra questi diversi approcci, assume particolare rilevanza quelo delle tecnologie server side, basate sul protocollo HTTP. Sono infatti quelle che più si prestano allo sviluppo di applicazioni web centrate sulla gestione di basi di dati. Le tecnologie Java, nate per ultime, hanno usufruito delle precedenti idee ed esperienze, apportando però anche significativi miglioramenti. L’uso di Servlet infatti ha apportato il vantaggio di avere un minore impiego di risorse, dato che tutti i Servlet sono eseguiti dalla stessa Java virtual machine, messa in esecuzione dal server, come thread distinti e non come processi distinti. E proprio come i Servlet, JSP regala gli stessi vantaggi legati all’uso di Java, e una reale portabilità in tutte le piattaforme in cui esista una implementazione della macchina virtuale Java (quindi, praticamente tutte quelle comunemente usate). In tutte le tecnologie server side, si è detto, si ha la generazione di pagine web come risultato di una determinata richiesta da parte del client. La pagina non esiste come risorsa statica, ma viene generata dinamicamente. Il problema sta nel fatto che chiamare funzioni o metodi di output che scrivano letteralmente codice HTML può portare alla generazione di continui errori. Con le Java Server Pages l’intero processo di generazione del codice, compilazione ed esecuzione è gestito automaticamente in modo trasparente. JSP ha anche la peculiarità di permettere l’inserimento diretto di codice Java nella pagina. Ma entriamo più nel dettaglio. Da un punto di vista funzionale, una pagina JSP si può considerare come un file di testo scritto secondo le regole di un markup language in base al quale il contenuto del file viene elaborato da un JSP container (ovvero, un software per l’elaborazione di pagine JSP), per restituire il risultato di una trasformazione del testo originale, secondo le istruzioni inserite nel testo. Si 89 3.5 Le Java Server Pages (JSP) tratta quindi di pagine web a contenuto dinamico generato al momento in cui la pagina viene richiesta dal client. Una pagina JSP contiene essenzialmente due tipi di testo: template text ovvero testo “letterale” destinato a rimanere tale dopo l’elaborazione della pagina; JSP text porzioni di testo che vengono interpretate ed elaborate dal JSP container (Tomcat). Nella maggior parte dei casi, il template text è in formato HTML (o a volte XML), dato che le JSP sono pagine web. Dal punto di vista del programmatore Java, una JSP viene vista come un modo particolare per interfacciarsi a oggetti Java. Il JSP container infatti converte la pagina JSP in un Servlet, generando prima il codice sorgente, poi compilandolo. Vediamo un semplice (anzi, banale!) esempio di pagina JSP: <html> <head> <title>Data e ora</title> </head> <body> <p> Data e ora corrente:<br> <b><%= new java.util.Date()%></b> </p> </body> </html> Si tratta, come si può vedere, di una normale pagina HTML, con un solo elemento estraneo a questo linguaggio <%= new java.util.Date()%>. Si tratta di una espressione JSP in linguaggio Java che verrà interpretata e valutata dal JSP container e sostituita dalla data e ora corrente. Quindi con new java.util.Date() viene creato un oggetto rappresentante l’istante attuale. Giacchè è richiesta una rappresentazione stringa, allora l’oggetto è convertito in una stringa1 Finora si sono visti soltanto alcuni degli elementi di una pagina JSP: il template text e le espressioni. Ora verrà fatta una breve descrizione di tutti gli elementi: 1 Per essere precisi, la conversione in Stringa è ottenuta automaticamente invocando per l’oggetto il metodo ereditato da Object toString(), in questo caso sovrascritto per rappresentare la data in forma di stringa. 90 3.5 Le Java Server Pages (JSP) 91 Template Text tutte le parti di testo che non sono definite come elementi JSP; vengono copiate tali e quali nella pagina di risposta. Comment con sintassi: <%- - comment - -%>; sono commenti che riguardano la pagina JSP in quanto tale, e pertanto vengono eliminati dal JSP container nella fase di traduzione-compilazione; da non confondere con i commenti HTML e XML, che vengono inclusi nella risposta come normale template text. Directive con sintassi: <%@ directive ...%>; sono direttive di carattere generale, indipendenti dal contenuto specifico della pagina, relative alla fase di traduzione-compilazione. Action seguono la sintassi dei tag XML ossia <tag attributes> body </tag> oppure <tag attributes/> dove attributes sono nella forma attr1 = value1 attr2 = value2 ...; le azioni sono eseguite nella fase di processing, e danno origine a codice Java specifico per la loro esecuzione. Sripting element sono porzioni di codice in uno scripting language specificato nelle direttive; il linguaggio di default è lo stesso Java; si dividono in tre sottotipi: Scriplet con sintassi: <% code %>; sono porzioni di codice nello scripting language che danno origine a porzioni di codice Java; generalmente inserite nel metodo service() del Servlet; se contengono dichiarazioni di variabile, queste saranno variabili locali valide solo nell’ambito di una singola esecuzione del Servlet; se il codice contiene istruzioni che scrivono sullo stream di output, il contenuto mandato in output sarà inserito nela pagina di risposta nella stessa posizione in cui si trova lo scriptlet. Declaration con sintassi <%! declaration [declaration] ...%>; sono dichiarazioni che vengono inserite nel Servlet come elementi della classe, al di fuori di qualunque metodo. Possono essere sia variabili di classe che metodi. Expression con sintassi <%= expression %>; contengono un’espressione che segue le regole dele espressioni dello scripting language; l’espressione viene valutata e scritta nella pagina di risposta nella posizione corrispondente a quella dell’espressione JSP. Torniamo all’esempio della pagina JSP per vedere come essa viene elaborata dal JSP container. In primo luogo, viene generato il codice che definisce una classe Java; questa classe è una Servlet, e il codice generato sarà il seguente: 3.5 Le Java Server Pages (JSP) class JSPDataOra extends HttpJspServlet { public void _jspService (HttpServletRequest request, HttpServletResponse response) { ... PrintWriter out = response.getWriter(); out.println("<html>"); out.println(); out.println("<head>"); out.println("<title>DataÃeÃOra</title>"); out.println("</head>"); out.println(); out.println("<body>"); out.println("<p>"); out.println("DataÃeÃoraÃcorrente:<br>"); out.println("<b>"); out.println(new java.util.Date()); out.println("</b>"); out.println("</p>"); out.println("</body>"); out.println(); out.println("</html>"); ... } } La classe è derivata da una ipotetica classe HttpJspServlet, che potrà essere una sottoclasse di HttpServlet e dovrà implementare l’interfaccia HttpJspPage, definita nella JSP API. Questa interfaccia contiene il metodo jspService(), corrispondente al metodo service() del Servlet generico. Il Servlet non fa altro che rimandare in output le parti del file che non contengono tag JSP come string literal mentre l’espressione JSP viene eseguita e valutata prima di essere scritta nello stream. Prescindendo dai dettagli implementativi, l’esecuzione di una pagina JSP prevede due fasi distinte: 1. fase di traduzione-compilazione, durante la quale viene costruito un oggetto costruibile dalla VM (in genere un Servlet) in grado di elaborare una risposta alle richieste implicite o esplicite contenute nella pagina; 92 3.5 Le Java Server Pages (JSP) 2. fase di processing, in cui viene mandato in esecuzione il codice generato e viene effettivamente elaborata e restituita la risposta. Il Servlet manipola una serie di oggetti per svolgere il suo lavoro, principalmente i due oggetti ServletRequest e ServletResponse, su cui si basa tutto il meccanismo di funzionamento. Quindi, per poter usufruire dei servizi del Servlet nella pagina JSP, occorre avere un modo per accedere a questi oggetti: a questo scopo esistono gli oggetti impliciti JSP, utilizzabili in tutti gli scriptlet. Eccone un elenco. Request Corrisponde generalmente all’oggetto ServletRequest passato come parametro al metodo service(); può essere una qualunque sottoclasse di ServletRequest; generalmente si tratta di una sottoclasse di HttpServletRequest. Response Corrisponde all’oggetto ServletResponse passato come parametro al metodo service() del Servlet; può essere una qualunque sottoclasse di ServletResponse; generalmente si tratta di una sottoclasse di HttpServletResponse. Out Poichè il container deve fornire un meccanismo di buffering, non è dato accesso direttamente all’oggetto PrintWriter restituito da response.getWriter(). L’oggetto out è invece uno stream bufferizzato di tipo javax.servlet.jsp.JspWriter, restituito da un metodo del PageContext. Il trasferimento sullo output stream del ServletResponse avviene in un secondo tempo, dopo che tutti i dati sono stati scritti sull’oggetto out. Page É un riferimento all’oggetto che gestisce la richiesta corrente, che si è vista essere generalmente un Servlet. In Java corrisponde al this dell’oggetto, pertanto è di scarsa utilità. Si presume risulti utile con altri linguaggi di script. PageContext Si tratta di un oggetto della classe javax.servlet.jsp.PageContext utilizzata prevalentemente per incapsulare oggetti e features particolari di ciascuna implementazione del container. Il PageContext viene utilizzato anche per condividere oggetti tra diversi elementi. Session Corrisponde all’oggetto HttpSession del Servlet, viene restituito da un metodo del PageContext. 93 3.5 Le Java Server Pages (JSP) 3.5.1 Esempio Lista Prodotti (carrello) Questo esempio mostra l’utilizzo di JSP e i vantaggi rispetto alle servlet quando occorre creare dinamicamente pagine Web complesse in cui la maggior parte del contenuto è statico. L’esempio mostra una bozza di un possibile sito per l’acquisto di prodotti on-line. Tutte le pagine JSP fanno uso della classe Prodotto che forniscono due metodi statici per ottenere la lista di tutti i prodotti acquistabili e per ottenere le informazioni di un prodotto attraverso il nome. Per semplicità l’esempio assume che non possano esistere due prodotti con lo stesso nome e quindi il nome può essere preso come “chiave”. Il repository è implementato come un file di testo. L’interazione inizia dalla pagina JSP nel Listato 3.14 (vedi Figura 3.6) dove vengono visualizzate la lista dei prodotti esistenti. Si osservi come la tabella inserita in una form sia generata dinamicamente. Anche lo stesso campo di testo è generato dinamicamente con il nome uguale al prodotto che rappresenta. L’utente inserisce nei campi di testo le quantità desiderate dei singoli prodotti e preme il bottone di submit. A questo i parametri delle form (cioè il nome dei prodotti con le loro quantità) sono passati alla pagina JSP addCarrello.jsp (vedi Listato 3.15 e Figura 3.7). La seconda pagina JSP utilizza l’attributo “Carrello” della sessione per memorizzare una Hashtable che contiene le associazioni tra le chiavi (i nomi dei prodotti) e i valori (le quantità nel carrello). La pagina JSP carrello.jsp mostra il resoconto dei prodotti nel carrello. Essa fa uso dell’attributo “Carrello” della sessione per ottenere le informazioni sui prodotti nel carrello. 94 3.5 Le Java Server Pages (JSP) package exampleJSP; import java.io.*; import java.util.*; public class Prodotto { private String nome; private float prezzo; private String marca; private Prodotto () {} public String getNome () { return (nome); } public float getPrezzo () { return (prezzo); } public String getMarca () { return (marca); } public static Vector listaProdotti() {/*Per ogni prodotto crea un oggetto Prodotto ed aggiungilo al vector*/ Vector lista = new Vector(); String nome; try { BufferedReader riga = new BufferedReader (new FileReader ("C:\\ListaProdotti.txt")); nome = riga.readLine(); while(nome!=null) { Prodotto p = new Prodotto(); p.nome = nome; p.marca = riga.readLine(); p.prezzo = Float.parseFloat(riga.readLine()); lista.addElement(p); nome = riga.readLine(); } } Listato 3.12: Classe Prodotto 95 3.5 Le Java Server Pages (JSP) catch (IOException err) { return null; } return(lista); } public static Prodotto getProdottoByName(String unNome) { try { String nome; FileReader in = new FileReader ("C:\\ListaProdotti.txt"); BufferedReader riga = new BufferedReader (in); nome = riga.readLine(); while(!nome.equals(unNome) && nome!=null) { riga.readLine(); riga.readLine(); nome = riga.readLine(); } if (nome.equals(unNome)) { Prodotto p = new Prodotto(); p.marca = riga.readLine(); p.prezzo = Float.parseFloat(riga.readLine()); p.nome = nome; return (p); } else return null; } catch (IOException err) { return null; } } } Listato 3.13: Classe Prodotto (continuo) 96 3.5 Le Java Server Pages (JSP) <html> <%@ page import = "java.util.*,ÃexampleJSP.*" %> <% Vector listaProdotti=Prodotto.listaProdotti(); %> ... <body>...<form method="GET" action="addCarrello.jsp"> <table border="1" width="100%" id="table1" height="172"> <tr> <td align="center" height="20"> <b><font size="2">Nome</font></b> </td> <td align="center" height="20"> <b><font size="2">Marca</font></b> </td> <td align="center" height="20"> <b><font size="2">Prezzo</font></b> </td> <td align="center" width="148" height="20"> <b><font size="2">àQuantit</font></b> </td> </tr> <% int size=listaProdotti.size(); for (int i=0;i<size;i++) { Prodotto p=(Prodotto)listaProdotti.get(i); %> <tr> <td><%= p.getNome() %></td> <td><% out.print(p.getMarca()); %></td> <td><% out.print("EuroÃ"+p.getPrezzo()); %></td> <td> <p><input type="text" name="<%=Ãp.getNome()Ã%>" size="20" value="0"></p> </td> </tr><% } %> </table> <p align="center"><button>AGGIUNGI</button></p> </form> ... </body></html> Listato 3.14: homepage.jsp 97 3.5 Le Java Server Pages (JSP) Figura 3.6: La Homepage Figura 3.7: La pagina Aggiunti al Carrello 98 3.5 Le Java Server Pages (JSP) <html> <%@ page import = "java.util.*,ÃexampleJSP.*" %> ... <body> <p align="center"><i> <font size="4"> Sono stati aggiunti al carrello i seguenti prodotti: </font></i></p> <% Enumeration e = request.getParameterNames(); int counter = 0; Hashtable carrello = (Hashtable)session.getAttribute("Carrello"); if(carrello==null) { carrello = new Hashtable(); session.setAttribute("Carrello", carrello); } while(e.hasMoreElements()) { String nome = (String)e.nextElement(); int quantita = Integer.parseInt(request.getParameter(nome)); %> <ul> <% if(quantita!=0) { out.println("<li>"+quantita+"Ã"+nome+"</li>"); counter = counter+1; Integer q = (Integer)carrello.get(nome); int v=0; if (q!=null) v=q.intValue(); Integer vquantita = new Integer(v+quantita); carrello.put(nome, vquantita); } %> </ul> <% } if(counter==0) out.println("NonÃe’ÃstatoÃselezionatoÃalcunÃprodotto!"); %> ...</body></html> Listato 3.15: addCarrello.jsp 99 3.5 Le Java Server Pages (JSP) <html> <%@ page import = "java.util.*,ÃexampleJSP.*" %> ... <body> <font size="4"> Il carrello contiene attualmente i seguenti prodotti:</font> <table border="1" width="100%" id="table1"> <tr> <td align="center"> <b><font size="2">Nome</font></b> </td> <td align="center"> <b><font size="2">Marca</font></b> </td> <td align="center"> <b><font size="2">Prezzo unitario</font></b> </td> <td align="center"> <b><font size="2">Quantita</font></b> </td> </tr> <% Prodotto prodotto; Hashtable carrello = (Hashtable)session.getAttribute("Carrello"); Enumeration enum = carrello.keys(); while(enum.hasMoreElements()) { String nome = (String)enum.nextElement(); prodotto = Prodotto.getProdottoByName(nome); out.println("<tr><td>"); out.println(nome); out.println(prodotto.getMarca()); out.println("</td><td>"); out.println(prodotto.getPrezzo()); out.println("</td><td>"); out.println(carrello.get(nome)); out.println("</td>"); out.println("</tr>"); } %> </table> ...</body></html> Listato 3.16: carrello.jsp 100 3.5 Le Java Server Pages (JSP) Figura 3.8: La pagina del Carrello 101 Capitolo 4 JMS: Enterprise Messaging (ed introduzione su MOM) Figura 4.1: Schematizzazione di un Message Oriented Middleware 102 4.1 Introduzione al MOM (richiami) 4.1 103 Introduzione al MOM (richiami) Con il termine messaging si definisce un meccanismo che permette la comunicazione asincrona tra client (detti peer-element) mediante scambio di messaggi caratterizzato da uno scarso grado di accoppiamento. Data questa definizione si può considerare messaging system un qualsiasi sistema che scambi pacchetti TCP/IP tra programmi client. Piuttosto che creare un meccanismo di messaging specifico per ogni situazione che richieda lo scambio di dati, diversi produttori hanno creato un sistema che permette di scambiare messaggi tra applicazioni diverse in modo generico. Questi sistemi sono chiamati Message Oriented Middleware (MOM). Tramite l’infrastruttura middleware del MOM (Figura 4.1), processi residenti su macchine diverse possono interagire tra loro mediante l’invio e ricezione di messaggi tipicamente asincroni. È importante notare che si è in presenza di un sistema di messaging peer-to-peer dove i client interagiscono tra loro non in modo diretto, bensı̀ mediante un messaging server, che fa le veci del “postino”: riceve i messaggi dai mittenti (“produttori”) e li recapita ai relativi destinatari (“consumatori”). Grazie alla mediazione del messaging server, i client risultano essere disaccoppiati, cioè non devono sapere nulla l’uno dell’altro. Il disaccoppiamento nei MOM è una caratteristica molto importante ed è maggiore rispetto ai sistemi middleware basati su architettura ORB (Object Request Broker). Si pensi ad esempio a RMI o CORBA: per poter comunicare con un oggetto remoto, un’applicazione deve essere a conoscenza dei suoi metodi, in modo da poterli invocare. Nei sistemi MOM, i client non interagiscono direttamente tra di loro. Inoltre né il produttore né il ricevitore devono essere a conoscenza l’uno dell’altro. L’unica cosa che entrambi devono sapere è il formato del messaggio e la “casella di posta” (destination) da usare. Altra importante caratteristica offerta dai sistemi MOM è il concetto di Guaranteed Message Delivery (GMD) che garantisce la consegna del messaggio. Al fine di prevenire una perdita di informazioni in caso di crash del message server, i messaggi devono essere resi persistenti (per esempio mediante JDBC) prima di essere recapitati ai consumatori. 4.1.1 Modelli di Messaging I Message-oriented middleware defiscono due diversi paradigmi di scambio di messaggi (Figura 4.2): point-to-point e publish and subscribe. Il modello di messaging point-to-point (p2p) è caratterizzato dal fatto che più produttori e più consumatori possono condividere la stessa destinazione (queue) e ogni messaggio ha un solo consumatore; questo permette di fatto una comunicazione uno-a-uno. Il modello publish/subscribe (pub/sub) permette 4.1 Introduzione al MOM (richiami) 104 Figura 4.2: I Modelli di Messaging supportati da JMS una comunicazione uno-a-molti dal momento che ogni messaggio inserito nella destinazione (topic) può essere inoltrato a tutti i destinatari che si sono dichiarati (subscription) interessati. Point-to-Point Nel modello p2p un client può mandare uno o più messaggi a un altro client.La comunicazione non avviene in modo diretto, bensı̀ condividendo una stessa destinazione verso cui si inviano e da cui si prelevano i messaggi. La destinazione nel modello point-to-point prende il nome di queue (“coda”). Più produttori e più consumatori possono condividere la stessa coda ma ogni messaggio ha un solo consumatore; questo permette di fatto una comunicazione uno a uno. Non ci sono dipendenze temporali tra sender e receiver del messaggio e quindi il ricevente può ricevere il messaggio anche se non era in ascolto sulla coda al momento dell’invio. Il messaggio viene tolto dalla coda una volta che il ricevente l’ha prelevato confermando (acknowledge) la ricezione al produttore. L’utilizzo del messaging p2p è da utilizzare nel caso si abbia l’esigenza che ogni messaggio spedito sia ricevuto da un solo consumatore che ne confermi l’avvenuta ricezione. PUB/SUB Nel modello publish/subscribe (detto anche sistema di messag- 4.2 Introduzione a JMS 105 ing event-driven), i client inviano messaggi a un topic, una coda “speciale” in grado di inoltrare i messaggi a più destinatari. Il sistema si prende carico di distribuire i messaggi inviati (publish) da più publishers: tali messaggi sono automaticamente ricevuti da tutti i client che hanno effettuato la sottoscrizione (subscribe), cioè che si sono dichiarati interessati alla ricezione. Il modello pub/sub permette quindi una comunicazione uno-a-molti visto che ogni messaggio può avere più consumatori. È il MOM che si occupa di recapitare una copia di ogni messaggio pubblicato agli appropriati destinatari. Il messaggio una volta consumato da tutti i subscriber interessati viene tolto dal topic. I client possono essere dinamicamente publisher oppure subscriber o anche entrambi. Esiste una dipendenza temporale tra publisher e subscriber, visto che il receiver può “consumare” il messaggio solo dopo essersi “sottoscritto” (subscription) al topic d’interesse, una volta che il client abbia effettivamente inviato il messaggio. Inoltre il receiver deve rimanere attivo, se vuole continuare a ricevere. Molti MOM (tra cui JBOSS) rilassano tale vincolo temporale mediante il concetto di durable subscriptions. Le durable subscriptions infatti permettono di ricevere i messaggi anche se i subscribers non erano in ascolto al momento dell’invio del messaggio. L’utilizzo di messaging publish/subscribe è da preferire quando si ha la necessità che ogni messaggio sia ricevuto da più consumatori. Le destinazioni sono accessibili in modo concorrente e si possono definire anche dei filtri per esprimere le condizioni da rispettare affinché un messaggio venga recapitato a una certa destinazione. 4.2 Introduzione a JMS Java Message Service (JMS) è un insieme di API Java che permette di creare, spedire, ricevere e leggere messaggi. JMS è stato sviluppato da Sun insieme ai maggior produttori di sistemi per definire un’interfaccia comune indipendente dalla specifica implementazione del sistema di messaging, in modo analogo a quanto avviene con JDBC, JNDI, JCA e cosı̀ via. L’applicazione Java JMS risulta cosı̀ essere indipendente, oltre che dal sistema operativo, anche dalla specifica implementazione del sistema di messaging. Mediante JMS è quindi possibile interagire con sistemi MOM esistenti quali MQSeries di IBM, Fiorano MQ, Microsoft MQ. Si capisce come lo scarso disaccoppiamento rispetto ai sistemi RPC, l’intrinseca asincronia delle operazioni di messaging, la sua integrazione in ambito J2EE (i Message Driven Bean) e l’indipendenza dello specifico provider rendono JMS una tecnologia potente, flessibile e fondamen- 4.2 Introduzione a JMS 106 Figura 4.3: Gli Attori JMS tale soprattutto per interazioni business-to-business (B2B) e integrazioni di sistemi eterogenei in ambito EAI (Enterprise Application Integration). In un’applicazione JMS gli attori coinvolti sono ConnectionFactory, Destination, Connection, Session, MessageProducer, Message e MessageConsumer, come rappresentato in Figura 4.3. Di seguito viene descritto brevemente il compito di ciascun attore JMS. Administered Objects Le Destination (queue/topic) e le ConnectionFactory sono dette administered objects perché incapsulano le specifiche implementazioni dei provider JMS. Gli administered objects sono vendor-dependent, cioè la loro specifica implementazione varia da provider a provider e per questo motivo non sono gestiti all’interno del programma Java. Le interfacce JMS permettono allo sviluppatore Java di astrarsi dai dettagli implementativi della specifica versione di MOM, permettendo il riutilizzo del codice al variare dell’implementazione JMS, rispettando appieno la filosofia Java. La creazione di ConnectionFactory e di Destination è compito dell’amministratore JMS che deve inserirli all’interno di un contesto JNDI in un opportuno namespace in modo che possano essere recuperati da qualsiasi client mediante le normali API JNDI. Per inviare messaggi (sender/publisher) o per riceverli (receiver/subscriber), un’applicazione JMS client utilizza i servizi JNDI per 4.2 Introduzione a JMS 107 ottenere un oggetto di tipo ConnectionFactory e uno o più oggetti di tipo Destination (Queue o Topic). Connection Factory È l’administered object utilizzato dal client per ottenere una connessione con il message server. Nel caso point-to-point si utilizza l’interfaccia QueueConnectionFactory: Context ctx = new InitialContext(); QueueConnectionFactoryqueueConnectionFactory = (QueueConnectionFactory)ctx. lookup("MyQueueConnectionFactory"); mentre nel caso di publish/subscribe si utilizza l’interfaccia TopicConnectionFactory: TopicConnectionFactory topicConnectionFactory = (TopicConnectionFactory)ctx. lookup("MyTopicConnectionFactory"); Destination È l’administered object che rappresenta l’astrazione di una particolare destinazione. Anche in questo caso il client identifica la destinazione mediante l’utilizzo delle API JNDI. Nel caso point-to-point, la destinazione è rappresentata dall’interfaccia Queue: Queue queue = (Queue) jndiContext.lookup("MyQueue"); mentre, nel caso di publish/subscribe, si usa l’interfaccia Topic: Topic topic = (Topic) jndiContext.lookup("MyTopic"); Connection Rappresenta l’astrazione di una connessione attiva con un particolare JMS provider e si ottiene dall’oggetto ConnectionFactory. Nel caso point-to-point, si ottiene un reference d’interfaccia QueueConnection invocando il metodo createQueueConnection sull’oggetto Connection Factory: Analogamente, nel caso di publish/subscribe, si ottiene un reference d’interfaccia TopicConnection invocando il metodo createTopicConnection() sull’oggetto ConnectionFactory: 4.2 Introduzione a JMS 108 TopicConnection topicConnection = topicConnectionFactory.createTopicConnection(); Session L’interfaccia Session si ottiene dall’oggetto Connection e rappresenta il canale di comunicazione con una destinazione: permette la creazione sia di produttori che di consumatori di messaggi. La creazione della sessione permette di specificare il controllo della transazione e la modalità di acknowledge. Nel caso point-to-point, la Session è identificata mediante l’interfaccia QueueSession: QueueSession queueSession = queueConnection. createQueueSession(false,Session.AUTO_ACKNOWLEDGE); Nel caso di publish/subscribe, la Session viene identificata tramite interfaccia TopicSession: TopicSession topicSession = topicConnection. createTopicSession(false,Session.AUTO_ACKNOWLEDGE); Messages I messaggi JMS sono costituiti da tre parti: un’intestazione (message header ), una serie di campi contenenti proprietà (property fields) e il corpo (message body). Il message header è costituito esattamente da 10 campi obbligatoriamente presenti. Tali campi sono valorizzati dal provider o dal client al fine di identificare, configurare e inoltrare il messaggio. Tra questi campi è presente l’identificatore univoco del pacchetto (JMSMessageID), il nome della Queue o Topic a cui il pacchetto è destinato(JMSDestination),la priorità (JMSPriority), il timestamp del tempo d’inoltro(JMSTimestamp), il flag indicante se il messaggio è stato re-inoltrato(JMSRedelivered), l’ID per correlare i messaggi JMS tra di loro (JMSCorrelationID), la modalità di invio(JMSdeliveryMode), la destinazione a cui inoltrare il messaggio di risposta(JMSReplyTo). I campi JMSReplyTo e JMSCorrelationID e il campo opzionale JMSType sono assegnati dall’applicazione JMS in modo esplicito sull’oggetto Message; tutti gli altri, invece, dal provider JMS. I property fields sono coppie di nome e valore e sono utili per costruire “filtri” a livello applicativo. La caratteristica di questi campi è che possono essere esaminati mediante i message selectors. Quando 4.2 Introduzione a JMS 109 un consumatore si connette al server può definire un message selector il quale esamina l’header e i property fields (non il body message) del pacchetto JMS di una specifica destinazione sia essa una Queue o un Topic. Questo permette alle applicazioni JMS di utilizzare i message selector per specificare quali messaggi è interessata a ricevere utilizzando una sintassi condizionale sottoinsieme del linguaggio SQL-92. Il filtraggio avviene a livello di server e permette di inoltrare ai client lungo la rete i messaggi strettamente necessari o utili, risparmiando cosı̀ la banda del canale. Un message selector non è altro che un oggetto di classe String che può contenere boolean literals(TRUE, true, FALSE e false), operatori logici (NOT, AND, OR), aritmetici (+, -, *, /), di comparazione( =, >, >=, <, <=, < >) di ricerca (IS, LIKE, IN, NOT IN) e cosı̀ via. Esempi di message selector: "squadra = ’Pittsburgh Steelers’" "squadra <> ’Dallas Cowboys’" "age NOT BETWEEN 20 and 32" "type = ’FIAT’ AND color = ’blue’ AND weight >= 100" "NewsType = ’Opinion’ OR NewsType = ’Sports’" I property field possono essere impostati mediante la classe di metodi setXXXProperty(String name, XXX value) dove XXX possono essere Boolean, Byte, Short, Integer, Long, Float, Double e String e rappresenta il tipo del campo proprietà. Ovviamente esistono i getter corrispondenti XXX getXXXProperty(String name) che restituiscono i valori delle proprietà a partire dal loro nome. Infine le proprietà possono essere cancellate mediante il metodo clearProperties(), lasciando il messaggio JMS senza campi proprietà. Body Message I messaggi JMS possono essere di varie tipi, ognuno dei quali mette a disposizione gli appositi metodi get e set. JMS non supporta uno specifico body per messaggi XML; in questi casi è utilizzabile il text message. I messaggi vengono creati mediante i metodi dell’interfaccia Session dalla quale estendono sia l’interfaccia QueueSession che TopicSession. L’interfaccia mette a disposizione i metodi di creazione relativi a ogni tipologia di message: createTextMessage, createObjectMessage, createMapMessage, createBytesMessage e createStreamMessage, prevedendo per ognuno di essi la versione overloaded che contempla come parametro in ingresso un oggetto di inizializzazione. Per creare messaggi, nel caso di tipologia point-to-point è sufficiente invocare il metodo create opportuno sulla QueueSession, mentre nel caso di pub- 4.2 Introduzione a JMS 110 lish/subscribe sull’oggetto TopicConnection come mostrato negli esempi che seguono: TextMessage txtMsg = queueSession.createTextMessage(); txtMsg.setText("HelloÃJMSÃPTPÃWorld!"); TextMessage txtMsg2 = queueSession.createTextMessage("HelloÃJMSÃPTPÃWorld!"); ObjectMessage objMsg = queueSession.createObjectMessage(); objMsg.setObject(my_ptp_order); ObjectMessage objMsg2 = topicSession.createObjectMessage(); ObjMsg2.setObject(my_pubSub_chat_msg); MessageProducer Il MessageProducer è l’oggetto che permette l’invio dei messaggi verso una particolare destinazione. Utilizzando gli oggetti Session e Destination creati, si possono inizializzare uno o più message producer. Nel caso point-to-point, il MessageProducer per inviare messaggi sulla Queue è referenziato mediante l’interfaccia QueueSender: QueueSender queueSender = queueSession.createSender(queue); mentre nel caso di publish/subscribe l’invio dei messaggi verso il Topic può avvenire mediante l’interfaccia TopicPublisher: TopicPublisher topicPublisher = topicSession.createPublisher(topic); Una volta creato il produttore, basta invocare il metodo send sull’oggetto QueueSender nel caso PTP, mentre nel caso pub/sub, il metodo publish(String message) va invocato sull’oggetto topicPublisher; entrambi i metodi send richiedono in ingresso un parametro d’interfaccia Message e presentano firme overloaded. queueSender.send(message); topicPublisher.publish(message); MessageConsumer Il MessageConsumer rappresenta un oggetto in grado di ricevere messaggi. Analogamente a quanto avviene per il MessageProducer, bisogna indicare l’oggetto Session e la Destination d’interesse per inizializzare correttamente il MessageConsumer. 4.2 Introduzione a JMS 111 public class TextListener implements MessageListener { public void onMessage(Message message) { try { if (message instanceof TextMessage) { TextMessage txtMsg = (TextMessage) message; System.out. println("Ricevuto:Ã" + txtMsg.getText()); } } catch(JMSException err) { System.err.println("ErroreÃJMS:"+err.getMessage()); } } } Listato 4.1: Esempio di Consumatore Asincrono QueueReceiver queueReceiver = queueSession.createReceiver(queue); TopicSubscriber topicSubscriber = topicSession.createSubscriber(topic); Nella modalità asincrona, il MessageProducer (sender o publisher) non è tenuto ad attendere che il messaggio sia ricevuto per proseguire nel suo funzionamento, e l’elaborazione del messaggio stesso non è necessariamente sequenziale. Il MessageConsumer è in grado di ricevere i messaggi in modo asincrono definendo un message listener : si tratta di una classe che implementa l’interfaccia javax.jms.MessageListener. Ogni qualvolta un messaggio arriva alla destinazione d’interesse viene automaticamente invocato il metodo di callback onMessage(). Il Listato 4.1 mostra l’implementazione di un consumatore asincrono: TextListener è una normale classe Java che implementa l’interfaccia JMS MessageListener ridefinendo perciò il metodo onMessage(). In tale metodo si mette la logica applicativa per gestire il contenuto del messaggio ricevuto; si effettua il controllo del tipo del messaggio ricevuto e, se è del tipo corretto, se ne elabora il contenuto. 4.2 Introduzione a JMS 112 Per associare il consumatore alla coda opportuna occorre creare un oggetto d’interfaccia javax.jms.MessageListener: TopicListener topicListener = new TextListener(); QueueListener queueListener = new TextListener(); e passarlo come argomento al metodi setMessageListener dell’oggetto Receiver topicSubscriber.setMessageListener(topicListener); queueReceiver.setMessageListener(queueListener); Connesso il listener, ci si pone in ascolto, in attesa di ricevere ed elaborare i messaggi, invocando il metodo start sull’oggetto Connection: queueConnection.start(); topicConnection.start(); I prodotti di messaging sono intrinsecamente asincroni ma è bene specificare che un MessageConsumer può ricevere i messaggi anche in modo sincrono. Nella ricezione dei messaggi in modo sincrono il consumatore richiede esplicitamente alla destinazione di prelevare il messaggio (fetch) invocando il metodo receive. Il metodo receive() appartiene all’interfaccia javax.jms.MessageConsumer (dalla quale estendono sia QueueReceiver che TopicReceiver) ed è sospensivo, cioè rimane bloccato fino alla ricezione del messaggio, a meno che non si espliciti un timeout finito il quale il metodo termina: public Message receive() throws JMSException public Message receive(long timeout) throws JMSException Esempio di ricezione sincrona: while(<condition>) { Message m = queueReceiver.receive(1000); if( (m!=null)&&(m instanceof TextMessage) { message = (TextMessage) m; System.out.println("Rx:" + message.getText()); } } Finite le operazioni di gestione del messaggio ricevuto, si esegue il cosiddetto codice di “pulizia” chiudendo la connessione JMS mediante il metodo close. 4.2 Introduzione a JMS queueConnection.close(); topicConnection.close(); 113 Capitolo 5 Enterprise Java Bean (EJB) Gli Enterprise Java Bean (EJB) sono componenti di un’architettura lato server utilizzati per semplificare il processo di sviluppo delle applicazioni a componenti in Java. Infatti, usando gli EJB è possibile produrre facilmente e velocemente applicazioni server-side e applicazioni scalabili per sistemi distribuiti. Il vantaggio di poter disporre di tali componenti risiede nel fatto che in questo modo è possibile concentrarsi meglio nella scrittura del codice necessario per la propria applicazione e quindi nella realizzazione di una applicazione server robusta piuttosto che sullo sviluppo del middleware. 5.1 Introduzione agli EJB Essendo gli Enterprise JavaBeans (EJB) componenti standard della tecnologia server-side in Java è possibile utilizzarli su qualsiasi tipologia di applicazione server. Per tale motivo c’è stata una notevole diffusione nell’utilizzo degli EJB, si è cercato quindi di facilitarne la comprensione e l’apprendimento, ma soprattutto si è resa particolarmente efficiente la loro portabilità a seguito della pubblicazione delle specifiche. Una critica che si può fare verso tali componenti è che questi devono essere scritti esclusivamente in linguaggio java. Questa, che apparentemente potrebbe sembrare una restrizione, in realtà è compensata dal fatto che Java è un linguaggio ideale nella costruzione di componenti, per diverse ragioni tra cui: Separazione interfaccia/implementazione: Secondo il paradigma MVC, la separazione tra interfaccia e implementazione consente maggiore modularità. Lo strato di presentation è separato, dunque posso aggiornare il mio portale senza aggiornare la logica o viceversa. Gli EJB 114 5.1 Introduzione agli EJB 115 non sono come i componenti GUI (Graphical User Interface), cioè non sono visibili agli utenti, anzi agiscono dietro di essi ed eseguono il vero lavoro. La differenza principale tra GUI ed EJB sta nel loro dominio applicativo. I GUI sono componenti lato client, cioè agiscono direttamente per il client creando interfacce grafiche o piccole operazioni logiche. Gli EJB, d’altra parte, sono componenti lato server, effettuano operazioni server come eseguire complessi algoritmi o eseguire un gran numero di transazioni; i loro componenti hanno bisogno di essere sempre in esecuzione e forniscono inoltre un sicuro ambiente multiutente transazionale. In un paradigma MVC (Model View Controller), attraverso gli EJB viene implementato lo strato di logica applicativa, cioè le applicazioni vere e proprie che realizzano le funzionalità del sistema. Per questo gli EJB non prevedono interazione diretta con i client. Per accedere agli EJB vengono utilizzati i GUI (che implementano la parte del Controller). Sicurezza: L’architettura Java è molto più sicura dei tradizionali linguaggi di programmazione sotto diversi punti di vista. Infatti con il linguaggio Java si hanno perdite di dati in memoria meno frequenti, maggior robustezza. Per esempio, se durante una comunicazione cade un Thread realizzato in linguaggio Java, l’applicazione continua la sua esecuzione. Inoltre Java comprende una ricca serie di librerie, che permette agli sviluppatori di non reinventare codice riducendo il rischio di creare errori. Portabilità: Java gira su qualsiasi piattaforma. Questo significa che gli EJB non hanno problemi di portabilità, e quindi possono essere utilizzati senza alcun problema anche da coloro che hanno investito su diverse piattaforme (UNIX, win32 e mainframes). Generalmente gli EJB sono utilizzati in modo specifico per aiutare a risolvere business problems. Per tale ragione essi possono eseguire qualunque dei seguenti task: Eseguire business logic: Cioè implementare la logica di un’applicazione server side, per esempio attraverso l’utilizzo della JavaMail API, gli EJB possono essere utilizzati ad esempio per elaborare il pagamento delle tasse di un’attività, per verificare i permessi degli utenti che approvano degli ordini di acquisto o mandano una conferma di un’ordine.. Accedere ad un database: L’accesso al database avviene attraverso l’uso delle Java Database Connectivity (JDBC) API. Tale accesso può essere 5.2 Fondamenti degli EJB 116 richiesto per esempio per immettere un ordine di alcuni libri, trasferire del denaro tra due conti bancari. Accedere ad un altro sistema: Gli EJB ottengono questa integrazione con il Java Connector Architecture (JCA). Un esempio di tale integrazione può essere il collegamento ad un sistema COBOL che elabora il fattore di rischio per una nuova assicurazione. 5.2 Fondamenti degli EJB Un Enterprise Bean può essere composto da uno o più oggetti Java, in quanto un componente potrebbe essere composto da più di un oggetto semplice. La tecnologia EJB si basa su altre due tecnologie: java RMI-IIOP e JNDI. I client di un EJB possono riferirsi ad esso senza preoccuparsi di come sia composto, cioè può essere utilizzato come una scatola nera. In particolare il funzionamento avviene nel seguente modo: il client del bean utilizza una semplice interfaccia pubblica la quale deve contenere le stesse specifiche del bean; tali specifiche obbligano il bean ad avere alcuni metodi definiti, i quali permettono all’EJB Container di maneggiare i bean senza preoccuparsi di quale sia in esecuzione. Un EJB è sempre contenuto in un EJB Container. I Container amministrano i componenti in modo da ottimizzare l’uso di risorse (es. pooling). Un contenitore EJB è responsabile per mediare l’accesso di un EJB alle risorse. Chiunque può chiamare un enterprise bean, una servlet, un’applet o un’altro bean. Quest’ultimo caso può dar vita ad una lunga catena di chiamate. Tale compito, notevolmente complesso se realizzato manualmente dal programma seguendo tutte le chiamate, può essere suddiviso utilizzando una varietà di bean già scritti che gestiscono i sotto compiti. Esistono 3 tipi diversi di EJB: Session Beans. Rappresentano le sessioni di interazione con i client. Possono eseguire qualunque tipo di azione, dal sommare i numeri ad accedere ad un database o chiamare un altro Bean. I Session Bean hanno la caratteristica di essere transienti: il loro stato non viene memorizzato in modo persistente in un database o simili. Entity Beans. Sono i dati di un’applicazione, ne rappresentano gli oggetti di business, (es. prodotti agli ordini, numeri di carta di credito di un’applicazione di eCommerce). Tipicamente gli entity bean sono gestiti dai session bean per raggiungere un obiettivo. Sono memorizzati in modo 5.2 Fondamenti degli EJB 117 Figura 5.1: Catena di invocazioni da applicazioni Swing persistente spesso, un’istanza di entity Bean corrisponde ad una tupla di un DB. Message-Driven Beans. I Message-Driven Beans sono simili ai session bean. La differenza principale riguarda l’attivazione, in particolare essi possono essere attivati solo tramite l’invio di messaggi, infatti i Message-Driven Beans restano in ascolto su un topic/coda JMS e si attivano solo quando un messaggio è inviato a quel topic/coda. Possono essere utilizzati per chiamare un altro enterprise beans. Un aspetto rilevante di tale tecnologia è dato dalla possibilità di incrociare le chiamate tra bean (Figura 5.1) ovvero quando un client chiama in modo sincrono un bean, quest’ultimo potrebbe aver bisogno di alcuni dati, per eseguire i propri metodi, che sono reperibili solo attraverso un’ulteriore chiamata ad un entity bean. La stessa modalità di incrociare le chiamate può avvenire anche con l’uso dei message-driven bean, i quali possono essere attivati a seguito della ricezione di un messaggio da un client su una coda/topic JMS, e come i session bean possono invocare successivamente gli entity bean per eseguire l’applicazione. Queste azioni possono essere eseguite anche attraverso browser internet grazie alle JSP/Servlet, i quali possono invocare i session bean o i message bean. Queste azioni sono mostrate in Figura 5.2 La tecnologia EJB, come l’RMI, è basata su oggetti distribuiti. La Figura 5.3 mostra come avviene il collegamento tra un client ed un oggetto distribuito, in particolare, essa mostra come: 1. Il client chiama lo stub, che è un oggetto proxy client-side. Lo stub è responsabile di mascherare la comunicazione sulla rete dal client, attraverso l’uso delle sockets, e quando necessario, attraverso la conver- 5.2 Fondamenti degli EJB 118 Figura 5.2: Catena di invocazioni da pagine Internet tramite JSP/Servlet Figura 5.3: Chiamata di un oggetto distribuito 5.3 Scheletro di un Enterprise Bean 119 sione dei parametri nella rappresentazione della rete, lo stub comunica attraverso la rete. 2. Lo stub chiama lo skeleton, il quale è un oggetto proxy server-side. Lo skeleton maschera le comunicazioni sulla rete dell’oggetto distribuito, riceve una chiamata dalla socket e converte i parametri ricevuti in un’opportuna rappresentazione Java. 3. Lo skeleton passa la chiamata all’oggetto distribuito, il quale esegue dei compiti. Una volta eseguiti tali compiti, tale oggetto passa di nuovo il controllo allo skeleton, il quale successivamente passa la chiamata dapprima allo stub e poi di nuovo al client. Un punto chiave è che sia lo stub che l’oggetto distribuito implementano la stessa interfaccia (Remote Object), questo significa che lo stub clona la segnalazione dei metodi, e il client che invoca i metodi sullo stub crede in realtà di eseguire i metodi dell’oggetto distribuito. Generalmente gli oggetti distribuiti sono complessi da costruire interamente, per tale motivo ci sarà bisogno dei servizi Middleware, come per esempio quelli per la gestione delle transazioni e della sicurezza. Ci sono due diversi tipi di Middleware: Esplicito e Implicito. La differenza principale tra l’una e l’altra tipologia risiede nella quantità di codice da scrivere; infatti, nel primo caso bisogna invocare direttamente i middleware API necessari al raggiungimento dell’obiettivo, nel secondo caso, invece, la richiesta dei servizi viene delegata ad un Request Interceptor che provvederà a chiamare i servizi middleware richiesti. Ciò avviene attraverso la creazione di uno speciale file descrittore. 5.3 Scheletro di un Enterprise Bean Un Enterprise Bean non è un file monolitico, ma è costituito da un’insieme di file che lavorano per creare il bean. 5.3.1 Enterprise Bean Class La prima parte del bean è costituita dall’implementazione vera e propria, il vero oggetto distribuito, cioè quel file che contiene i metodi della logica di business, chiamato Enterprise Bean Class. Questa è una classe Java conforme ad una interfaccia well-defined e obbedisce a regole certe che permettono al bean di poter essere eseguito in ogni Container. 5.3 Scheletro di un Enterprise Bean 120 public interface javax.ejb.EnterpriseBean extends Serializable { } Listato 5.1: L’interfaccia javax.ejb.EnterpriseBean Le specifiche dell’EJB definiscono poche interfacce standard che il bean può implementare, le quali forzano il bean a fornire dei metodi, come definito dal modello dei componenti EJB. L’EJB Container chiama questi metodi per allertare o informare il bean di alcuni eventi significativi. L’interfaccia base che tutti i bean devono implementare è la javax.ejb.EnterpriseBean, mostrata nel Listato 5.1 Questa interfaccia contrassegna il bean come un vero enterprise bean class. L’aspetto interessante di questa interfaccia è che estende java.io.Serializable, ciò significa che tutti gli enterprise bean possono essere convertiti in un flusso di byte e condividere tutte le proprietà degli oggetti serializabili. Session Beans, Entity Beans e Message-driven Beans hanno tutti un’interfaccia che estende la javax.ejb.EnterpriseBean. Tutti i Session Bean devono implementare la javax.ejb.SessionBean; tutti gli Entity Bean devono implementare la javax.ejb.EntityBean; tutti i Message-driven Bean devono implementare la javax.ejb.MessageDriverBean. Quindi il bean non deve implementare direttamente la javax.ejb.EnterpriseBean, ma piuttosto implementare l’interfaccia corrispondente al proprio tipo. 5.3.2 L’EJB Object Quando un client vuole usare un’istanza dell’enterprise class, non può invocare mai direttamente l’istanza. Piuttosto l’invocazione viene intercettata dall’EJB Container e in seguito delegata all’istanza del bean. Questo è il concetto del request interceptor accennato in precedenza. Grazie ad esso, l’EJB Container può eseguire automaticamente il middleware implicito. Tale modo di operare facilita lo sviluppo perchè non si deve scrivere, correggere o mantenere il codice che chiama i middleware API, quindi, il container agisce come un livello intermedio tra il cliente e il bean. Questo livello viene chiamato EJBObject. L’EJBObject viene generato automaticamente dal container in quanto cambia per ogni bean Remote Interface Il client invoca i metodi sull’EJBObject, piuttosto che sul bean, per tale motivo l’EJBObject deve clonare tutti i metodi che il bean espone. Questo 5.3 Scheletro di un Enterprise Bean 121 public interface javax.ejb.EJBObject extends java.rmi.remote { public javax.ejb.EJBHome getEJBHome() throws java.rmi.RemoteException; public java.lang.Object getPrimaryKey() throws java.rmi.RemoteException; public void remove() throws java.rmi.RemoteException, javax.ejb.RemoveException; public javax.ejb.Handle getHandle() throws java.rmi.RemoteException; public boolean isIdentical(javax.ejb.EJBObject) throws java.rmi.RemoteException; } Listato 5.2: L’interfaccia javax.ejb.EJBObjeect avviene attraverso un’interfaccia speciale: la Remote Interface. Questa interfaccia deve essere conforme alle regole definite dalle specifiche EJB; per esempio: tutte le remote interface devono derivare da una interfaccia comune, cioè dalla javax.ejb.EJBObject descritta nel Listato 5.2 Questi metodi verranno implementati dal container e non dal client; la remote interface estende anche java.rmi.Remote, ciò fornisce la possibilità di chiamare tale oggetto da un’altra Java Virtual Machine. La remote interface deve essere conforme alla convenzione RMI-IIOP dei parametri passati. 5.3.3 L’Home Object Si è visto che il client invoca direttamente l’EJBObject invece del bean, in realtà ciò può avvenire solo se il client ha modo di avere il riferimento a questo oggetto. In particolare, il client ottiene il riferimento attraverso una fabbrica di EJBObject: l’Home Object. Quest’oggetto è responsabile della creazione, ricerca e rimozione di un EJBObject. Come l’EJBObject, l’home object è specifico per ogni bean. La differenza è che per ogni bean si ha un’unica istanza dell’Home Object, invece gli EJB Object sono tanti quanti sono i client che richiedono l’uso di quel bean. 5.3 Scheletro di un Enterprise Bean 122 public interface javax.ejb.EJBHome extends java.rmi.remote { public EJBMetaData getEJBMetaData() throws java.rmi.RemoteException; public void remove(javax.ejb.Handle) throws java.rmi.RemoteException, javax.ejb.RemoveException; public javax.ejb.HomeHandle getHomeHandle() throws java.rmi.RemoteException; public void remove(Object) throws java.rmi.RemoteException, javax.ejb.RemoveException; } Listato 5.3: L’interfaccia javax.ejb.EJBHome Home Interface L’Home Interface permette al client di inizializzare correttamente un EJBObject. Per esempio se un EJBObject ha due costruttori, uno che prende in ingresso un intero e l’altro che prende in ingresso una stringa, tramite l’home interface il client sceglie quale costruttore usare, e come ritorno si ha il puntatore all’EJBObject mascherato dalla remote interface. Come per la remote interface, l’home interface deve estendere l’interfaccia javax.ejb.EJBHome illustrata nel Listato 5.3 L’home object deriva dalla java.rmi.Remote, il che significa che può essere invocato attraverso la rete. Inoltre, i tipi di parametri passati nei metodi della home interface devono essere tipi validi per la Java RMI-IIOP. 5.3.4 Le interfacce locali Un problema rilevante che si riscontra con l’utilizzo delle home interface e delle remote interface è nella lentezza della loro creazione. Di seguito sono riportati tutti i passi che devono essere eseguiti al fine di creare una home interface: 1. Il client chiama lo stub locale. 2. Lo stub converte i parametri nel formato adatto alla rete. 5.3 Scheletro di un Enterprise Bean 123 3. Lo stub si connette attraverso la rete allo skeleton. 4. Lo skeleton riconverte i parametri nel formato adatto a Java. 5. Lo skeleton chiama l’EJB object 6. L’EJB object esegue i middleware necessari 7. Una volta che l’EJB object chiama l’istanza dell’enterprise bean e il bean esegue il suo lavoro, ogni passo precedente deve essere ripetuto per ritornare al client. Come si può notare è un percorso piuttosto laborioso; per tale motivo un modo più efficiente per invocare un EJB è riferirsi alle interfacce locali: local object invece che EJB object e local interface al posto della remote interface. In tal caso il processo di chiamata attraverso le interfacce locali sarà: 1. Il client chiama un local object. 2. Il local object esegue le operazioni necessarie. 3. Una volta che l’enterprise bean ha finito il suo lavoro restituisce il controllo al local object che lo restituisce al client. E’ evidente che con tale approccio si salta tutta la parte relativa alla rete, dalle chiamate allo stub e allo skeleton fino al marshalling/unmarshalling dei parametri. Le local interface estendono la javax.ejb.EJBLocalObject mentre le home interface estendono la javax.ejb.EJBLocalHome. Il codice di queste classi è mostrato nel Listato 5.4 Lo svantaggio nell’utilizzo di tali interfacce locali sta nel fatto che esse non estendono le classi RMI e quindi, per tale motivo, non possono essere chiamate da client remoti ma solo da bean presenti nello stesso processo, inoltre, il passaggio dei parametri non avviene per valore ma per riferimento e ciò comporta un cambio nella semantica dell’applicazione client. 5.3.5 Deployment Descriptor Per comunicare al container quali middleware utilizzare si devono dichiarare i servizi che il componente richiede in un deployment descriptor file. 5.3 Scheletro di un Enterprise Bean public interface javax.ejb.EJBLocalObject { public javax.ejb.EJBLocalHome getEJBLocalHome() throws javax.ejb.EJBException; public java.lang.Object getPrimaryKey() throws javax.ejb.EJBException; public void remove() throws javax.ejb.EJBException; javax.ejb.RemoveException; public boolean isIdentical(javax.ejb.EJBLocalObject) throws javax.ejb.EJBException; } public interface java.ejb.EJBLocalHome { public void remove/java.lang.Object) throws javax.ejb.EJBException; javax.ejb.RemoveException; } Listato 5.4: Interfacce locali 124 5.4 Session Bean 5.4 125 Session Bean Un Session Bean rappresenta una sessione di interazione con i client. Esso implementa la logica di business, le regole di business e gli algoritmi. E’ un componente riusabile che contiene i processi della logica di business. Un Session Bean rappresenta un singolo client nell’Application Server. Il client accede al Business Layer di una applicazione invocando i metodi dei Session Beans. I Session Beans nascondono la complessità della logica di business dietro a chiamate semplici. Un’importante differenza tra Entity Bean e Session Bean sta nel fatto che i session bean sono componenti a vita breve, mentre gli entity bean possono vivere per anni perche’ sono oggetti persistenti e fanno parte di sistemi di salvataggio permanente (come ad esempio i database). La vita di un session bean dura il tempo dell’esecuzione dei metodi richiesti dal client. Generalmente la lunghezza della sessione del client determina il tempo in cui il session bean sara’ in uso. L’EJBContainer ha l’autorità di distruggere i session bean se il client va in time out. Tipicamente i session bean non sopravvivono ai crash dell’applicazione e sicuramente non sopravvivono ai crash della macchina. I session bean sono non persistenti, cio’ significa che non vengono salvati su un archivio fisico. Essi possono eseguire azioni su database ma non possono essere salvati su di esso. Tutti gli enterprise bean ottengono una conversazione con i client. Una conversazione è un’interazione tra client e bean, ed è composta da una serie di metodi chiamati tra il client e il bean. Esistono due tipi di session bean: stateful session bean e stateless session bean, ognuno dei quali modella un tipo diverso di conversazione. Stateless Session Bean Le conversazioni di alcuni processi si limitano semplicemente ad una sola richiesta. Un singolo processo non ha bisogno di mantenere uno stato tra le diverse invocazioni dei metodi. Dopo ogni invocazione, il container può decidere di distruggere il bean, ricrearlo o pulirlo di tutte le informazioni contenute su di esso nelle passate invocazioni. Un tipico esempio di stateless session bean è un motore ad alte prestazioni che risolve complesse operazioni matematiche, come la compressione o la decompressione di dati audio o video. Dal momento che i session bean non hanno stato, tutte le istanze dello stesso bean sono equivalenti e indistinguibili dal client. Non importa quale istanza è stata invocata da un client specifico, ogni stateless session bean può servire ogni richiesta di un client. 5.4 Session Bean 126 Stateful Session Beans In alcuni processi è necessario ricordare lo stato della conversazione, cioè lo stato del sistema. Per esempio potrebbe essere necessario ricordare cio’ che abbiamo immesso nel carrello della spesa di un sito web. I bean di tipo stateful contengono variabili che hanno il compito di mantenere lo stato della sessione del client. Quando un client invoca un metodo sul bean, il client fa partire una conversazione con il bean. Questa conversazione viene salvata dal bean e deve essere accessibile ad ogni chiamata dello stesso client. Per limitare il numero di bean presenti in memoria (per non sovraccaricare la memoria fisica del computer), l’EJB container esegue l’operazione di pooling. Questa operazione consiste nell’ esecuzione di uno swap out dello stato della conversazione dalla memoria fisica nell’hard disk quando sono presenti troppi bean in memoria. Successivamente, quando il client richiede l’uso del bean, il container esegue uno swap in della conversazione nel bean. L’operazione di salvataggio del bean su un archivio permanente viene chiamata passivazione. L’operazione inversa viene chiamata attivazione. E’ da notare che, quando la conversazione viene riattivata, il bean potrebbe non essere quello originario. Questo non costituisce un problema in quanto cio’ che interessa non è il bean stesso ma lo stato della conversazione tra il bean e il client. Come si può immaginare, lo stato della conversazione deve seguire le regole della Java object serialization. Quando un bean viene messo in modalità passiva, il container usa il protocollo serialization per convertire lo stato della conversazione in un flusso di dati, formato che consente la scrittura su disco. L’istanza del bean continua ad esistere, e può essere assegnata ad un’altra conversazione con un nuovo client. Il processo di attivazione è l’inverso: un dato che è stato salvato su hard disk viene riletto, riportato in memoria e riconvertito nella modalità appropriata. L’interfaccia Serializable, che l’enterprise bean indirettamente implementa, permette tali processi. Quando l’EJB container passa una conversazione in modalità passiva, informa il bean chiamando uno dei metodi che devono essere implementati obbligatoriamente: ejbPassivate(). Questo metodo notifica al bean che la conversazione sta per essere trasferita su disco rigido. In questo modo il bean può rilasciare le risorse che in altri momenti aveva occupato, come una connessione ad un database, l’apertura di una socket, un file aperto, o altre risorse che non possono essere salvate usando la serializzazione. Quello che accade durante l’attivazione è l’esatto opposto. Lo stato della conversazione viene riportato in memoria, successivamente il container chia- 5.4 Session Bean 127 ma il metodo richiesto ejbActivate(), che dà l’opportunità di riaprire le risorse che erano state chiuse nella fase di passività. Nel caso in cui non ci sono risorse da rilasciare come le socket o le connessioni a database, non c’è bisogno di implementare questi metodi. 5.4.1 Esempi Nelle sezioni che seguono vedremo in pratica come viene creato un EJB attraverso alcuni esempi. Il primo esempio sarà un Session Bean Stateless: il bean avrà due metodi per la cifratura e la decifratura di una stringa secondo una chiave numerica inserita. Il secondo esempio sarà un semplice Session Bean Stateful: questo bean conterrà semplicemente il numero di chiamate da parte del client (un numero passato dal client oppure l’intero 1 in caso di nessuna comunicazione). Quando si crea un componente EJB, i tipici passi da eseguire sono i seguenti: 1. Scrivere le classi .java che compongono il bean: le interfacce remote, le home interface, la enterprise bean class e tutte le altre classi necessarie all’applicazione. 2. Scrivere il deployment descriptor. 3. Compilare le classi creando i file .class 4. Creare un file EJB-jar che contiene le classi java e il deployment descriptor, secondo uno schema che vedremo in seguito. 5. Installare il bean all’interno del Container (per esempio JBoss). 6. Configurare il container per le esigenze del bean (per esempio le connessioni al database o la dimensione del pooling). 7. Far partire il Container e verificare se il bean è stato installato correttamente. 8. Opzionalmente creare una applicazione stand-alone per testare la funzionalità effettiva del bean creato. 5.4.2 Ciclo vita dei Session Bean Session Bean Stateless Quando è stato realizzato il bean e installato all’interno del container, è quest’ultimo che si prende l’onere di gestire la sua vita, dalla creazione alla 5.4 Session Bean 128 Figura 5.4: Schema del ciclo vita di un stateless session bean cancellazione. In figura è mostrato lo schema del ciclo vita del Session Bean Stateless. Quando un client richiede la creazione di un Session Bean Stateless, il Container può agire in due modi differenti: - Creare un nuovo EJB e quindi aggiungerlo al pool dei Session Stateless. Questo comporta l’invocazione dei metodi 1 e 2 mostrati in Figura 5.4. Dopo l’invocazione del metodo ejbCreate, ovvero dopo che il client chiama il metodo create, il bean si trova nello stato Ready e quindi il client può invocare i business methods. - Restituire un bean già creato e presente nel pool. Il client non potrà mai sapere che non è stato creato in quanto i bean stateless sono indistinguibili, non hanno stato. Dopo l’utilizzo del bean e alla fine della sessione, il container può eliminare l’EJB dal pool, dopo aver invocato il metodo ejbRemove, oppure non effettuare nessuna operazione invocando il bean alla richiesta di un nuovo bean. Come si vede dallo schema, i metodi ejbActivate e ejbPassivate possono anche venir lasciati vuoti in quanto non verranno mai chiamati. Session Bean Stateful In Figura 5.5 è mostrato il ciclo di vita di un stateful session bean. Da ricordare che i metodi del bean sono chiamati dal container, non dal client. Questo ciclo di vita è molto simile a quello di un stateless session bean, ma ci sono alcune grandi differenze: 5.4 Session Bean 129 Figura 5.5: Schema del ciclo vita di un stateful session bean - Il container non può restituire un altro bean presente nel pool in quanto il suo stato appartiene ad una conversazione precedente e quindi diversa da quella scelta. - Esistono le transazioni tra lo stato attivo e passivo. Quando il client richiede la creazione di un Session Bean Stateless, l’EJB container crea un nuovo bean, usato solo da quel client, ed invoca i metodi 2 e 3 del grafico. Ora il bean è pronto a ricevere invocazioni remote per i suoi metodi di business. Mentre si trova nello stato attivo, il container può decidere di disattivare il bean, facendo lo swap in memoria secondaria, o eliminarlo. Quando avviene questa disattivazione, il container chiama il metodo ejbPassivate e successivamente lo disattiva. Se decide di eliminarlo, chiama prima il metodo ejbRemove e poi elimina il bean ed il suo stato. Quando il client invoca un metodo sul bean passivo, il container lo attiva, chiama il metodo ejbActivate, e lo riporta in modalità Ready. Se il bean si trova in modalità passiva può andare in timeout ed in tal caso il container elimina direttamente il bean. 5.4.3 Creazione di un Session Bean Stateless L’interfaccia remota Il primo passo consiste nello scrivere l’interfaccia remota. L’interfaccia remota duplica tutti i metodi implementati dall’enterprise bean class. Il codice è mostrato nel Listato 5.5 (e fa riferimento ad un’applicazione per la criptazione di una password). Ci sono alcune cose da notare: 5.4 Session Bean 130 package crypt; public interface Crypt extends javax.ejb.EJBObject { String crypt(String origine, int key) throws java.rmi.RemoteException; String decrypt(String criptata, int key) throws java.rmi.RemoteException; } Listato 5.5: L’interfaccia Remota - La classe estende javax.ejb.EJBObject. Ciò significa che l’oggetto generato dal container conterrà tutti i metodi che l’interfaccia javax.ejb.EJBObject definisce, quindi un metodo per comparare due EJB Object, un metodo per rimuovere un EJB Object, e cosı̀ via. - Nell’interfaccia remota appaiono due metodi, crypt(String,int) e decrypt(String,int), che devono essere implementati nella enterprise bean class. Dal momento che EJBObject estende java.rmi.Remote, anche l’interfaccia remota la estenderà e quindi dovrà lanciare la java.rmi.RemoteException. Inoltre, anche i parametri passati devono rispettare le regole RMI-IIOP, per questo i parametri che sono usati nell’esempio sono serializzabili (String é una classe che estende la serializzazione). La classe enterprise bean Il secondo passo è scrivere la classe che fornisce l’implementazione del servizio, cioè il vero Bean. Il codice è mostrato nel Listato 5.6. Bisogna notare che: - La nostra implementazione estende javax.ejb.SessionBean, che fa del bean un Session Bean. Questa interfaccia definisce alcuni metodi che la classe deve obbligatoriamente implementare. Il container usa questi metodi per interagire con il bean, invocandoli in caso di eventi importanti (come l’attivazione o la rimozione del bean). - Il bean ha un metodo ejbCreate() che il container chiama quando il bean viene creato. Questo metodo non può essere inserito nell’interfaccia javax.ejb.SessionBean, in quanto i parametri passati possono cambiare 5.4 Session Bean 131 package crypt; public class CryptBean implements javax.ejb.SessionBean{ public void ejbCreate() {} public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() {} public void setSessionContext(javax.ejb.SessionContext ctx) {} public String crypt(String origine, int key) { char dest[] = new char[origine.length()]; for (int i = 0; i < origine.length(); i++) dest[i] = (char) ( ( (int) origine.charAt(i)) + key); return (new String(dest)); } public String decrypt(String criptata, int key) { char dest[] = new char[criptata.length()]; for (int i = 0; i < criptata.length(); i++) dest[i] = (char) ( ( (int) criptata.charAt(i)) - key); return (new String(dest)); } } Listato 5.6: La classe enterprise bean 5.4 Session Bean 132 package crypt; public interface CryptHome extends javax.ejb.EJBHome { Crypt create() throws java.rmi.RemoteException, javax.ejb.CreateException; } Listato 5.7: La home interface oppure essere assenti come nell’esempio. I parametri qui passati devono essere gli stessi presenti nel metodo create() della Home interface. - I metodi ejbActivate() e ejbPassivate() vengono lasciati vuoti in quanto non ci sono risorse da ottenere o rilasciare nel caso di azioni del Container. - Quando il bean viene distrutto, non c’è nulla da rimuovere, quindi il metodo ejbRemove() può essere lasciato vuoto. - Se il Container deve comunicare con il bean, utilizza i metodi descritti in precedenza. Se invece il bean deve comunicare con il Container, può utilizzare la classe SessionContext. Il container chiama il metodo setSessionContext in caso di cambiamento di alcuni parametri. La Home interface Il passo successivo consiste nella creazione della home interface. La home interface ha i metodi per creare e distruggere gli EJB Object. Infatti il client chiamerà il metodo create() per avere la enterprise bean class, mascherata dall’interfaccia remota. Il codice per la home interface è mostrato nel Listato 5.7. Bisogna notare che: - Il metodo create() lancia una javax.rmi.RemoteException e javax.ejb.CreateException. La RemoteException è necessaria in quanto sono oggetti remoti che lavorano sulla rete. La CreateException è richiesta per ogni metodo create() - La classe Home interface estende javax.ejb.EJBHome, estensione richiesta a tutte le home interface, che definisce il modo per distruggere un EJB Object. 5.4 Session Bean 133 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD Enterprise JavaBeans 2.0//EN’ ’http://java.sun.com/dtd/ejb-jar_2_0.dtd’> <ejb-jar> <enterprise-beans> <session> <ejb-name>Crypt</ejb-name> <home>crypt.CryptHome</home> <remote>crypt.Crypt</remote> <ejb-class>crypt.CryptBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> </ejb-jar> Listato 5.8: Il deployment descriptor Il deployment descriptor Successivamente c’è bisogno di generare il deployment descriptor. Il deployment descriptor è una delle chiavi degli EJB in quanto permette di descrivere gli attributi specifici invece di programmare funzioni interne. Il deployment descriptor è un documento XML. Il deployment descriptor per il nostro esempio è mostrato nel Listato 5.8. <ejb-name> Questo è il nickname per uso interno. <home> Il nome completo della home interface (completo di package) <remote> Il nome completo del’interfaccia remota <ejb-class> Il nome completo della classe che implementa il session bean <session-type> Il tipo di session bean: Stateless o Stateful Il file jboss.xml Questo è un file specifico per ogni Container. Ci sono alcune differenze tra container, come gli algoritmi sul pooling, algoritmi sul clustering e altro. 5.4 Session Bean 134 <?xml version="1.0" encoding="UTF-8"?> <jboss> <enterprise-beans> <session> <ejb-name>Crypt</ejb-name> <jndi-name>crypt</jndi-name> </session> </enterprise-beans> </jboss> Listato 5.9: jboss.xml Per JBoss il file che descrive queste caratteristiche è chiamato jboss.xml ed è mostrato per il nostro esempio nel Listato 5.9. Anche questo file è in formato XML: <ejb-name> Questo è il nickname per uso interno. <jndi-name> Questo è il nome usato per il look-up JNDI Il file Ejb-jar Una volta creati questi file bisogna raggrupparli in un Ejb-jar, file che nel caso di JBoss e’ semplicemente una cartella che viene chiamata nomeEJB.jar (per esempio crypt.jar) I file .class devono venir generati inserendo nel classpath il package javax.ejb presente nella cartella: {dir. inst}\client\jboss-j2ee.jar (per esempio C:\Programmi\jboss-4.0.3SP1\client\jboss-j2ee.jar). Una volta creati i file .class bisogna inserirli nella cartella, insieme ai descriptor file, secondo lo schema in Figura 5.6 Successivamente inserire la cartella creata nella directory di installazione: {dir.inst}\server\default\deploy. Se JBoss è gia in esecuzione dopo qualche secondo compariranno tre righe come in Figura 5.7 Il client La scrittura del client è molto semplice, grazie alla trasparenza della programmazione EJB è possibile scrivere il codice senza preoccuparsi di dove verrà eseguito. Questo avviene grazie al looking up del JNDI. I passi da eseguire per la scrittura di un client sono semplici: 5.4 Session Bean 135 Figura 5.6: Scema del ejb-jar file Figura 5.7: Visualizzazione del deploy su JBoss 5.4 Session Bean 136 1. Effettuare il look-up per ottenere il riferimento alla home interface. 2. Usare la home interface per creare il riferimento al EJBObject. 3. Eseguire i metodi del bean per risolvere la propria applicazione di business. 4. Opzionalmente si può rimuovere il bean. Il container eliminerà automaticamente il bean dopo un certo periodo. Per poter compilare ed eseguire le classi .java, c’è bisogno dei package contenuti nel file {dir.inst}\client\jbossall-client.jar. Inoltre c’è bisogno della copia delle interfacce home e remote (Crypt.class e CryptHome.class) Nel caso in cui queste classi siano nella stessa directory del client (se non ci sono c’è bisogno di specificare la loro posizione nel classpath), il classpath per compilare i file .java potrebbe essere per esempio: C:\Programmi\jboss-4.0.3SP1\ client\jboss-all-client.jar;. La classe del client è riportata nel Listato 5.10. 5.4.4 Creazione di un Session Bean Stateful La remote interface Per prima cosa si implementa la remote interface illustrata nel Listato 5.11. Quest’interfaccia definisce un singolo metodo, add(), che implementerà la enterprise bean class. Questo metodo aggiunge una unità al contatore del bean e restituisce il valore aggiornato. La enterprise bean class Questa classe è responsabile dell’incremento della variabile intera val. In questo esempio lo stato della conversazione è semplicemente la variabile val definita all’interno della classe. Il codice della classe è mostrato nel Listato 5.12 e 5.13. Ci sono alcune cose da notare: - Esistono due metodi ejbCreate, uno prende come paramentro un intero, l’altro nessun parametro. Questi due metodi sono responsabili dell’inizio della conversazione, partendo da uno stato già definito o da un nuovo stato. - La variabile val, in quanto primitiva, è serializzabile e quindi è possibile salvare il suo stato. In questo modo lo stato della conversazione sopravvive alle diverse chiamate dei metodi ed è salvato automaticamente durante l’attivazione o durante la passivazione. 5.4 Session Bean 137 package client; import javax.naming.*; import java.util.Properties; import crypto.*; public class Index { public static void main(String args[]) throws Exception { Properties prop = new Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); prop.put(Context.URL_PKG_PREFIXES, "org.jnp.interfaces"); prop.put(Context.PROVIDER_URL, "localhost"); Context ctx = new InitialContext(prop); Object obj = ctx.lookup("crypt"); CryptHome home = (CryptHome) javax.rmi.PortableRemoteObject. narrow(obj, CryptHome.class); Crypto convert = home.create(); String cryptArgs0 = convert.crypt(args[0], 3); System.out.println("LaÃstringaÃcriptataèÃ:Ã" + cryptArgs0); cryptArgs0 = convert.decrypt(cryptArgs0, 3); System.out.println("LaÃstringaÃdecriptataèÃ:Ã" + cryptArgs0); } } Listato 5.10: Il client package statefullBean; public interface CountRemote extends javax.ejb.EJBObject{ public int add() throws java.rmi.RemoteException ; } Listato 5.11: L’interfaccia Remota 5.4 Session Bean 138 package statefulBean; public class CountBean implements javax.ejb.SessionBean { private int val; public int add() { val++; System.out.println("HaiÃchiamatoÃilÃmetodoÃdi incremento... ora stai a "+val+" chiamate"); return val; } public void ejbCreate(int val) throws javax.ejb.CreateException { this.val = val; System.out.println("CreazioneÃdelÃbean con valore val="+val); } public void ejbCreate() throws javax.ejb.CreateException { this.val = 0; System.out.println("CreazioneÃdelÃbean con valore val="+val); } public void ejbRemove() { System.out.println("IlÃbeanÃàverrÃoraÃrimosso"); } public void ejbPassivate() { System.out.println("IlÃbeanÃàverrÃmessoÃpassivoÃe il suo stato salvato su hard disk"); } Listato 5.12: L’Enterprise Bean 5.4 Session Bean 139 public void ejbActivate() { System.out.println("IlÃbeanèÃÃstatoÃattivatoÃe la sua conversazioneè stata ricreata"); } public void setSessionContext(javax.ejb.SessionContext ctx) {} } Listato 5.13: L’Enterprise Bean package statefullBean; public interface CountHome extends javax.ejb.EJBHome { public CountRemote create(int val) throws javax.ejb.CreateException, java.rmi.RemoteException; public CountRemote create() throws javax.ejb.CreateException, java.rmi.RemoteException; } Listato 5.14: La home interface - Questa classe implementa il metodo setSessionContext(). Nel nostro esempio non è utilizzato ma, come già spiegato nell’ esempio precedente, esso è il collegamento tra bean e Container. Attraverso questa classe il bean può comunicare con il Container. In Java è possibile passare il proprio riferimento usando la parola chiave this, in EJB non è possibile in quanto il bean viene chiamato indirettamente attraverso l’EJBObject. La home interface Per completare il codice del bean stateful, bisogna definire la home interface. Tale interfaccia specifica come creare e distruggere il bean. Il codice è mostrato nel Listato 5.14. Da notare le due ricorrenze del metodo create(),una per ciacun metodo ejbCreate nell’Enterprise Bean (Listato 5.12). 5.4 Session Bean 140 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD Enterprise JavaBeans 2.0//EN’ ’http://java.sun.com/dtd/ejb-jar_2_0.dtd’> <ejb-jar> <enterprise-beans> <session> <ejb-name>CountBean</ejb-name> <home>statefullBean.CountHome</home> <remote>statefullBean.CountRemote</remote> <ejb-class>statefullBean.CountBean</ejb-class> <session-type>Stateful</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> </ejb-jar> Listato 5.15: Il deploy descriptor Il deployment descriptor Ora che tutte le classi .java sono state create, c’è bisogno di definire il deployment descriptor per specificare al Container i settaggi del bean. Lo schema XML è esposto nel Listato 5.15. Si può notare che nel tag <Session-type> si specifica la natura del bean (Stateless o Statefull), evitando di introdurre la notazione all’interno del codice. In questo modo si semplifica lo scambio tra stateless e stateful. Il file jboss.xml Il file specifico per JBoss è mostrato nel Listato 5.16. Per completare l’installazione c’è bisogno della creazione del file ejb-jar. I passi da seguire sono i medesimi dell’esempio precedente. Un settaggio che è possibile cambiare nel Container è il numero massimo di bean attivi, cioè il pooling. E’ possibile modificarlo all’interno del file standardjboss.xml che si trova nella directory {dir.inst}\server\default\conf. All’interno di questo file bisogna cercare il tipo di bean all’interno del tag <container-name> (per esempio StandardStatefulSessionBean) e modificare 5.4 Session Bean 141 <?xml version="1.0" encoding="UTF-8"?> <jboss> <enterprise-beans> <session> <ejb-name>CountBean</ejb-name> <jndi-name>Count</jndi-name> <max-pool-size>2</max-pool-size> <min-pool-size>1</min-pool-size> </session> </enterprise-beans> </jboss> Listato 5.16: jboss.xml il tag <max-capacity>. Questo tag impone al container il numero massimo di bean presenti in memoria. Per fare in modo che la modifica venga accettata bisogna far ripartire JBOSS. Il client Ora che il bean è stato installato, possiamo scrivere il codice Java per testarlo. I passi da eseguire saranno i seguenti: 1. Acquisire un JNDI initial context. 2. Trovare l’home object attraverso il look-up JNDI 3. Creare tre diversi Count EJB object, quindi creare tre diverse conversazioni e simulare tre diversi client. 4. Limitare la bean pool capacity a due bean, in modo tale che durante la creazione delle tre conversazioni uno dei tre bean debba essere reso passivo. Per essere certi che questo avvenga,è stato inserito un messaggio all’interno del metodo ejbPassivate(). 5. Chiamare il metodo add() su ogni EJB object. Ciò forza il container ad attivare i bean messi precedentemente in modalità passiva. Come per il metodo precedente è stato inserito un messaggio all’interno del metodo ejbActive 6. Rimuovere i tre bean il codice è mostrato nei Listati 5.17 e 5.18. 5.4 Session Bean 142 package clientBean; import javax.naming.*; import statefullBean.*; public class Index { public static void main(String[] args) { try { java.util.Properties prop = new java.util.Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces"); prop.put(Context.PROVIDER_URL, "localhost"); Context ctx = new InitialContext(prop); Object obj = ctx.lookup("Count"); CountHome home = (CountHome) javax.rmi. PortableRemoteObject.narrow(obj, CountHome.class); CountRemote count[] = new CountRemote[3]; int countVal; System.out.println("inizializzazioioneÃdeiÃbean"); count[i] = home.create(); Listato 5.17: Il Client 5.4 Session Bean 143 package clientBean; System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã0"); count[i] = home.create(1); System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã1"); count[i] = home.create(3); System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã3"); System.out.println("chiamoÃilÃmetodoÃcountÃsuiÃbeans"); for (int i=0;i<3 ;i++) { countVal = count[i].add(); System.out.println("ContatoreÃa:Ã"+countVal); Thread.sleep(500); } for(int i = 0; i < 3 ; i++) { count[i].remove(); } } catch(Exception er) { er.printStackTrace(); } } } Listato 5.18: Il Client 5.5 Entity Bean 5.5 144 Entity Bean Gli Entity Bean sono oggetti persistenti che possono essere salvati in un dispositivo permanente, come un database. Questo significa che è possibile modellare i dati come un Entity Bean. I dati vengono salvati in un database, mappando una tupla come un oggetto. Questo avviene salvando ogni parte costituente il Bean in un campo della tabella. Quando si desidera caricare il Bean dal database, viene creato un nuovo Bean e riempito con i campi ottenuti dal database. Questa operazione di mappatura di un oggetto in un database relazionale è chiamata object relational mapping (O/R mapping). Si tratta di un’azione di conversione e riconversione da un oggetto in memoria ad un dato relazionale e viceversa. Si noti che questo meccanismo è molto diverso dalla serializzazione e quindi dal salvataggio su hard disk: decomponendo un oggetto è infatti possibile eseguire query per una qualsiasi informazione piuttosto che caricare tutto l’oggetto in memoria. In una qualunque applicazione object-oriented, anche la più sofisticata, è sempre possibile effettuare una distinzione tra due tipi di componenti: Applicazioni logiche. Questo tipo di componenti fornisce, attraverso l’uso dei Session Bean, metodi per l’esecuzione di azioni comuni. Contengono spesso algoritmi per la risoluzione di problemi e nascondono la complessità dietro semplici chiamate. Dati persistenti. Si tratta di oggetti che possono rappresentare dati semplici o complessi che devono essere salvati. Possono rappresentare, inoltre, persone, luoghi o cose. Questi componenti usano meccanismi persistenti, come la serializzazione o l’O/R mapping in un database relazionale o in un object database. I dati potrebbero essere sempre usati come righe di un database, ma è più vantaggioso usarli come oggetti perchè in questo modo risultano più compatti e sono quindi più facili da gestire. Inoltre, è possibile associare dati ad azioni, raggruppare i dati in oggetti e ottenere il middleware necessario per un’applicazione server. Gli Entity Bean sono componenti che rappresentano gli “oggetti di business” conservati in un meccanismo di memoria persistente (database relazionale). Sono completamente diversi dai Session Bean: questi ultimi modellano processi e azioni, mentre gli Entity Bean contengono i dati necessari per l’applicazione. Per esempio, un Session Bean potrebbe trasferire una certa quantità di denaro da un account ad un altro, mentre i dati di questi account sono rappresentati negli Entity Bean. 5.5 Entity Bean 145 Gli Entity Bean contengono un set di interfacce standard comune a tutti gli EJB, tra cui: interfacce locali e remote, local interface e local home interface, la classe enterprise bean e il deployment descriptor. Esistono notevoli differenze tra le interfacce e i file degli Entity Bean e quelli degli altri tipi di EJB: Le classi entity bean tracciano una definizione dell’entità in uno schema del database. Per esempio, un Entity Bean potrebbe essere mappato in una definizione di tabella relazionale. In questo caso, un’istanza di un Entity Bean della classe verrà salvata in una riga di una tabella. Inoltre, il Bean potrebbe fornire alcuni semplici metodi per manipolare dati o per accedervi, come decrementare il bilancio di un conto bancario. Come la classe session bean anche questa classe deve fornire alcuni metodi standard. La classe primary key fa si che ogni Entity Bean sia diverso. Per esempio, se abbiamo un milione di account bancari, ogni account ha bisogno di un ID unico che non può essere mai ripetuto in un altro account. Questa classe può contenere un numero qualsiasi di attributi. Gli EJB consentono di definire in maniera flessibile ciò che identifica il bean, compresa la classe primary key. L’unica regola è che questa classe deve essere serializzabile e seguire le regole della object serialization di Java. 5.5.1 Caratteristiche Gli Entity Bean sono resistenti a errori critici, come crash di applicazioni server o anche crash di macchina. Questo perchè sono rappresentazioni di dati in un sistema di salvataggio permanente e fault-tolerant. Se si verifica un crash del sistema o della macchina, l’Entity Bean può essere ricreato in memoria. Questa è la differenza fondamentale tra i Session e gli Entity Bean. Gli Entity Bean hanno un ciclo di vita molto più lungo di una sessione di un client, può durare anche anni, dipende da quanto tempo i dati sono sul database. La potenza di questo sistema risiede nel fatto che i dati situati in memoria e sul database possono essere visti come una cosa sola: cioè se un dato viene cambiato in memoria viene immediatamente cambiato anche nel database. Ovviamente, i dati in memoria sono la copia dei dati presenti sul database, quindi deve esserci un meccanismo di trasferimento di informazioni tra gli oggetti Java e il database. Questo trasferimento è compiuto con due metodi speciali che gli Entity Bean devono implementare: ejbLoad() e ejbStore(). 5.5 Entity Bean 146 L’EJBContainer usa questi metodi per manipolare gli EJB: il metodo ejbLoad() viene invocato quando i dati vengono letti dal sistema di salvataggio permanente in memoria, mentre il metodo ejbStore() viene invocato quando i dati devono essere salvati nel database. Il container si preoccupa di chiamare questi metodi: possono essere chiamati in qualunque momento (tranne che nei metodi di business). Inoltre, il container si preoccupa anche della sincronizzazione dei dati. Quando ci sono molti accessi agli stessi dati, il container crea istanze multiple della stessa classe Entity Bean. Questo permette a molti client di interagire concorrentemente con istanze separate, ognuna rappresentante i medesimi dati. Per mantenere la consistenza dei dati, i Bean devono essere sincronizzati costantemente. Il container sincronizza i dati chiamando i metodi ejbLoad() e ejbStore(). Questi metodi, come i metodi relativi al pooling (ejbActivate() ed ejbPassivate()), sono metodi che il container utilizza per comunicare al Bean che sta per essere caricato o salvato sul database. Anche gli Entity Bean sono soggetti al pooling. Invece di creare e distruggere un Bean quando un client si connette o disconnette, i Bean vengono riutilizzati in quanto la struttura del Bean non cambia e i dati possono essere ricavati dal database. Come accade per i Session Bean, il container utilizza i metodi ejbActivate() e ejbPassivate() per comunicare al Bean che verrà messo in modalità attivo o passivo e quindi che esso dovrà lasciare o recuperare risorse che non possono essere salvate (come le connessioni ad un database o ad una socket). Ovviamente quando un EJB viene reso passivo, oltre a rilasciare le risorse, deve anche salvare il proprio stato su database. Quindi verrà chiamato il metodo ejbStore(). In maniera duale, quando esso verrà attivato, oltre a riprendere le risorse deve ricreare il suo stato e quindi chiamare il metodo ejbLoad() Qualunque sia il tipo di supporto scelto per il salvataggio c’è bisogno di scrivere il codice di accesso al supporto per salvare o caricare i dati. Esistono due tipi di approcci: bean-managed persistent e container-managed persistent. Nel primo caso, è lo sviluppatore del Bean a scrivere tutto il codice necessario per trasferire i campi in memoria nel supporto di salvataggio scelto (un database relazionale o ad oggetti). Nel secondo caso, invece, è il Container che si fa carico di scrivere il codice necessario alle operazioni di salvataggio, caricamento e modifica dei dati. Come vedremo negli esempi, questo tipo di approccio riduce drasticamente la quantità di codice da scrivere. Siccome l’istanza di un Entity Bean e i dati sul database possono essere pensati come una cosa sola, l’inizializzazione di un Bean potrebbe richiedere l’inizializzazione di una tupla sul database. Quindi, quando un Bean viene creato con il metodo ejbCreate(), viene creata la corrispondente riga sul 5.5 Entity Bean 147 database. Cosı̀, in un approccio bean-managed persistent il metodo ejbCreate() è responsabile della creazione dei dati sul database. Dualmente, il metodo ejbRemove() è responsabile della rimozioni dei dati dal database. Nel caso, invece, container-managed persistent è il Container a modificare il database e i metodi create e remove possono essere lasciati vuoti dal codice di accesso al supporto di salvataggio (per esempio codice SQL). C’è da ricordare che il client non invoca direttamente il Bean. L’EJB object è generato attraverso l’home object. Quindi, ad ogni metodo ejbCreate() deve corrispondere il metodo create() nella home interface. Per esempio, consideriamo un Entity Bean che contenga un account bancario AccountBean con un’interfaccia remota Account, home interface AccountHome e primary key AccountPK. Dato questo metodo ejbCreate() in AccountBean: public AccountPK ejbCreate(String accountID,String proprietario)... il metodo Create() deve essere implementato nella home interface (da notare la mancanza del prefisso “ejb”): public Account Create(String accountID,String proprietario)... Il valore di ritorno dei due metodi è diverso. L’istanza del Bean ritorna la primary key AccountPK mentre l’home object ritorna un EJB object Account. Questo avviene in quanto il metodo ejbCreate() viene eseguito dal Container, il quale ha bisogno della primary key per identificare il bean. Con la primary key il container può generare l’EJBObject e restituirlo al client. Per distruggere i dati di un Entity Bean in un database, il client deve chiamare il metodo remove() sulla remote interface. Bisogna osservare che questo metodo non rimuove l’oggetto in memoria, ma solo i dati dal database. Ciò accade perchè gli oggetti in memoria vengono riutilizzati da un altro Bean. Il metodo ejbRemote() è un metodo presente nell’interfaccia e che deve quindi essere implementato. Poichè gli Entity Bean contengono informazioni che altri Bean possono acquisire, essi devono essere facilmente reperibili. Per individuare uno specifico Entity Bean è possibile eseguire diversi tipi di ricerca definiti come metodi e detti finder. La home interface contiene questi metodi addizionali, oltre ai metodi di creazione e di distruzione. 5.5.2 Esempi Di seguito saranno proposti due esempi che spiegheranno in dettaglio la differenza tra Entity Bean container-managed persistent e bean-managed persistent. Il primo esempio tratta la gestione di Account Bancari ed è stato 5.5 Entity Bean 148 public interface javax.ejb.EntityBean { public public public public public public public void void void void void void void setEntityContext(javax.ejb.EntityContext); unsetEntityContext(); ejbRemove(); ejbActivate(); ejbPassivate(): ejbLoad(); ejbStore(); } Listato 5.19: L’interfaccia javax.ejb.EntityBean realizzato con gli Entity Bean di tipo bean-managed persistent. Il secondo invece si occupa della Gestione di Prodotti ed è stato realizzato con un EJB container-managed persistent. 5.5.3 Creazione di un Entity Bean Bean-Managed persistent Quando si scrive questo tipo di bean, il programmatore deve fornire il codice necessario per l’accesso ai dati. Per scrivere un Entity Bean, bisogna scrivere un file Java che implementi l’interfaccia javax.ejb.EntityBean. Questa interfaccia, come le altre derivanti da javax.ejb.EnterpriseBean, definisce alcuni metodi che il Bean deve implementare. Il codice di questa interfaccia è mostrato nel Listato 5.19. Questi sono i metodi che il container invoca in caso di un evento. Tale interfaccia deve essere implementata da entrambi i tipi di Entity Bean. Ci sono anche altri metodi che si possono definire, come i metodi per creare nuovi Bean o individuare quelli già esistenti. I metodi ejbFind() I metodi ejbFind() non creano nuovi dati dal database, ma caricano dati dagli Entity Bean. Questi metodi vengono implementati solo in questo tipo di Bean, in quanto per i container-managed persistent i metodi in questione vengono automaticamente generati. Si possono avere diversi tipi di metodi finder. Una regola fondamentale è che tutti questi tipi di metodi devono iniziare con “ejbFind ”. Per esempio, ejbFindTuttiProdotti() oppure ejbFindNomeProdotto(). 5.5 Entity Bean 149 Un’altra regola consiste nel definire obbligatoriamente un metodo find : ejbFindByPrimaryKey(). Questo metodo, a differenza degli altri metodi che restituiscono una Collection, trova un’unica istanza dell’Entity Bean (basata sulla chiave primaria del Bean), e della tabella del database corrispondente. Cosı̀ come per il metodo ejbCreate(), il client non invoca direttamente questi metodi, ma invoca i corrispettivi metodi sulla home interface. Per far capire la corrispondenza tra questi metodi si prenda ad esempio il metodo public Collection ejbFindBigAccount (int unMinimo) throws FinderException. Il corrispettivo nella home interface sarà: public Collection findBigAccount(int unMinimo) throws FinderException (da notare il prefisso ejb). I metodi sopra descritti seguono tutti i seguenti passi: - Quando il client invoca il metodi find sulla home interface, l’home object chiede al Bean di trovare tutte le primary key class che corrispondono ai parametri richiesti. Il Bean restituisce una Collection di queste chiavi primarie al container. - Quando riceve la collezione di chiavi dal Bean, il container crea una collezione di EJB Object, uno per ogni chiave, e restituisce questa collezione di oggetti al client. Ogni EJB Object rappresenta l’istanza relativa ai dati contenuti nel database. La remote interface: Account.java La classe Account è la prima classe del nostro Bean, la remote interface. É mostrata nel Listato 5.20. Come tutte le remote interface, estende la javax.ejb.EJBObject. Inoltre, offre una serie di metodi per manipolare l’Entity Bean, come i metodi per effettuare il prelievo ed il deposito. Da notare l’AccountException, una eccezione creata per gestire gli errori di prelievo o di deposito o altri errori relativi all’account. L’home interface: AccountHome.java Questa classe ha la responsabilità di creare i dati sul database che rappresenta l’account bancario. Il metodo create() restituisce un EJB Object al client capace di manipolare i dati appena creati. La classe è mostrata nel Listato 5.21. Inoltre, sono presenti due metodi finder. findByPrimaryKey cerca all’interno del database se esiste l’account con chiave primaria AccountPK. findName cerca all’interno del database tutti gli account con nome “nomeProp”. 5.5 Entity Bean 150 package entityBean; import javax.ejb.*; import java.rmi.RemoteException; public interface Account extends EJBObject { public void deposit(double amt) throws AccountException,RemoteException; public void prelievo(double amt) throws AccountException,RemoteException; public double getBilancio() throws RemoteException; public String getNomeProprietario() throws RemoteException; public void setNomeProprietario(String unNome) throws RemoteException; public String getAccountID() throws RemoteException; public void setAccountID(String id) throws RemoteException; } Listato 5.20: La remote interface 5.5 Entity Bean 151 package entityBean; import javax.ejb.*; import java.rmi.RemoteException; import java.util.Collection; public interface AccountHome extends EJBHome { public Account create(String accountID, String nomeProp) throws RemoteException,CreateException; public Account findByPrimaryKey(AccountPK key) throws RemoteException,FinderException; public Collection findName(String nomeProp) throws RemoteException,FinderException; public double getTotalBankValue() throws AccountException,RemoteException; } Listato 5.21: La home interface 5.5 Entity Bean 152 Infine c’è il metodo getTotalBankValue() che restituisce l’ammontare dell’intera banca, ovvero la somma di tutti gli account presenti nel database. La primary key class: AccountPK.java Il codice di questa classe è mostrato nel Listato 5.22. Nel nostro caso, la chiave primaria si riduce ad una semplice stringa: AccountID. Questa stringa deve essere unica all’interno dell’account bancario. Nei casi più complessi, dove ci sono più campi che identificano la chiave primaria, questa classe conterrebbe diverse variabili. Il metodo toString() è un metodo richiesto. Il container chiama questo metodo per ricevere la descrizione della chiave primaria sotto forma di String. In questo caso, il metodo ritorna semplicemente il campo. In casi più complessi, bisogna combinare i vari campi per formare una stringa. Ci sono, inoltre, altri due metodi richiesti: hashCode() e equals(). Il primo viene utilizzato dal container in quanto può decidere di salvare la primary key class in una Hashtable. Questo può avvenire quando all’interno del container viene creata una lista di tutti gli Entity Bean in memoria. Il metodo equals() viene usato dal container per determinare, al suo interno, se due entity bean rappresentano gli stessi dati. L’enterprise bean class: AccountBean.java Il passo decisivo è creare l’implementazione dell’entity bean. L’implementazione di questo esempio è abbastanza lunga ed è stata suddivisa in 3 parti contenenti: 1. i campi che identificano lo stato del bean gestito. 2. i metodi di business che il client usa. Questi metodi sono esposti nella remote interface. 3. i metodi richiesti dall’interfaccia javax.ejb.EntityBean più i metodi find e i metodi per creare il bean definiti nella home interface. Questa classe è molto pesante e lunga in quanto ad essa compete tutta la gestione dei dati sul database. Il codice è mostrato dal Listato 5.23 al Listato 5.33. AccountException.java Questa classe è mostrata nel Listato 5.34. Questa è una semplice classe che delega alla classe padre java.lang.Exception gli errori sull’account bancario, 5.5 Entity Bean 153 package entityBean; public class AccountPK implements java.io.Serializable { private String accountID; public AccountPK(String ID) { accountID = ID; } public int hashCode() { return accountID.hashCode(); } public String toString() { return accountID; } public boolean equals(Object obj) { if(obj instanceof AccountPK) { if(((AccountPK)obj).accountID.equals(accountID)) return true; else return false; } else return false; } } Listato 5.22: La primary key class 5.5 Entity Bean 154 package entityBean; import import import import java.sql.*; javax.naming.*; javax.ejb.*; java.util.*; public class AccountBean implements EntityBean{ private EntityContext ctx; // Campi dello stato del bean private String accountID; private String nomeProp; private double bilancio; public AccountBean() { System.out.println("UnÃnuovoÃaccountèÃÃstatoÃcreato."); } Listato 5.23: AccountBean.java 5.5 Entity Bean 155 // Metodi della business logic public void deposit(double amt) throws AccountException { System.out.println("StoÃdepositandoÃl’ammontare di "Ã+ÃamtÃ+Ã" sul conto "Ã+ÃaccountID); bilancio += amt; } public void prelievo(double amt) throws AccountException { System.out.println("StoÃprelevandoÃ" + amt + " dal conto "Ã+ÃaccountID); if (amt > bilancio)throw new AccountException( "IlÃbilancionÃnonÃcopreÃilÃprelievo.ÃIlÃBilancioÃdel conto "Ã+ÃaccountIDÃ+Ã"è di: "Ã+ÃbilancioÃ+Ã"); bilancio -= amt; } // Metodi get e set dell’entity bean public double getBilancio(){ System.out.println("IlÃbilancioÃdelÃconto "+accountID+"è di "+bilancio+"); return bilancio; } public void setNomeProprietario(String unNome){ System.out.println("inseriscoÃilÃnomeÃproprietario"); nomeProp = unNome; } public String getNomeProprietario() { System.out.println("E’ÃstatoÃrichiestoÃilÃnomeÃdel proprietario del conto "+accountID); return nomeProp; } Listato 5.24: AccountBean.java 5.5 Entity Bean 156 public String getAccountID() { System.out.println("MièÃÃstatoÃrichiestoÃl’account di questo conto: "+accountID); return accountID; } public void setAccountID(String unAccount) { System.out.println("HoÃricevutoÃlaÃrichiesta di cambio dell’account:Ã\n dal vecchio "+accountID+" al nuovo "+unAccount); accountID = unAccount; } public double ejbHomeGetTotalBankValue() throws AccountException { PreparedStatement pst = null; Connection conn = null; try { System.out.println("E’ÃstatoÃchiamatoÃilÃmetodo per la visione completa del bilancio della banca"); conn = getConnection(); pst = conn.prepareStatement("select sum(bilancio) as total from accounts"); ResultSet rs = pst.executeQuery(); if(rs.next()) return rs.getDouble("total"); } Listato 5.25: AccountBean.java 5.5 Entity Bean 157 catch(Exception e) { e.printStackTrace(); throw new AccountException(e); } finally { try { if(pst!=null) pst.close(); if(conn!=null) conn.close(); } catch(Exception er){er.printStackTrace();} } throw new AccountException("errore!"); } // connessione al database interno a JBOSS public Connection getConnection() throws Exception { try { Context ctx = new InitialContext(); javax.sql.DataSource data = (javax.sql.DataSource) ctx.lookup("java:comp/env/jdbc/ejbPool"); return data.getConnection(); } catch(Exception er) { System.err.println("erroreÃnelÃcaricareÃilÃdatabase"); er.printStackTrace(); throw er; } } Listato 5.26: AccountBean.java 5.5 Entity Bean 158 // metodi richiesti dall’interfaccia public void ejbActivate(){ System.out.println("IlÃcontoÃ"+accountID+ "èÃÃstatoÃattivato"); } public void ejbPassivate(){ System.out.println("ilÃcontoÃ"+accountID+"èÃÃstato messo in àmodalit passivo"); } public void ejbRemove(){ throws RemoveException System.out.println("ilÃcontoÃ"+accountID+ "èÃÃstatoÃrimosso"); AccountPK pk = (AccountPK) ctx.getPrimaryKey(); String id = pk.toString(); System.out.println("laÃkiaveÃottentutaèÃ"+pk); PreparedStatement st = null; Connection conn = null; try { conn = getConnection(); st = conn.prepareStatement("deleteÃfromÃaccounts where id = ?"); st.setString(1,id); if(st.executeUpdate() == 0) throw new RemoveException("impossibileÃcancellare dal database l’accountÃ"Ã+Ãpk); } catch(Exception e) { throw new EJBException(e.toString()); } Listato 5.27: AccountBean.java 5.5 Entity Bean 159 finally { try { if(st!=null) st.close(); if(conn!=null) conn.close(); } catch(Exception er){er.printStackTrace();} } } public void ejbLoad() { AccountPK pk = (AccountPK) ctx.getPrimaryKey(); String id = pk.toString(); System.out.println("laÃkiaveÃottentutaèÃ" + pk); PreparedStatement st = null; Connection conn = null; try { conn = getConnection(); st = conn.prepareStatement("selectÃnomeProp,Ãbilancio from accounts where id = ?"); st.setString(1,id); ResultSet rs = st.executeQuery(); rs.next(); nomeProp = rs.getString("nomeProp"); bilancio = rs.getDouble("bilancio"); } catch (Exception er) { throw new EJBException("impossibileÃcaricareÃl’account "+pk+" dal database",er); } Listato 5.28: AccountBean.java 5.5 Entity Bean 160 finally { try { if(st!=null) st.close(); if(conn!=null) conn.close(); } catch(Exception er){er.printStackTrace();} } } public void ejbStore() { System.out.println("E’ÃstatoÃchiamatoÃilÃmetodoÃper il salvatagggio su database"); PreparedStatement st = null; Connection conn = null; try { conn = getConnection(); st = conn.prepareStatement("updateÃaccounts set nomeProp = ?, bilancio = ? where id = ?"); st.setString(1, nomeProp); st.setDouble(2,bilancio); st.setString(3,accountID); st.executeUpdate(); } catch (Exception er) { throw new EJBException("impossibileÃsalvareÃl’account "+accountID+" dal database",er); } Listato 5.29: AccountBean.java 5.5 Entity Bean 161 finally { try { if(st!=null) st.close(); if(conn!=null) conn.close(); } catch(Exception er){er.printStackTrace();} } } public void setEntityContext(EntityContext ctx) { System.out.println("chiamatoÃilÃmetodoÃsetEntityContext"); this.ctx = ctx; } public void unsetEntityContext() { System.out.println("chiamatoÃl’unsetÃdelÃentitycontext"); ctx = null; } public void ejbPostCreate(String accountID,String nomeProp) { System.out.println("chiamatoÃilÃmetodoÃpostCreate"); } public AccountPK ejbCreate(String accountID, String nomeProp) throws CreateException { PreparedStatement st = null; Connection conn = null; Listato 5.30: AccountBean.java 5.5 Entity Bean 162 try { System.out.println("CreazioneÃdelÃconto"); this.accountID = accountID; this.nomeProp = nomeProp; bilancio = 0; conn = getConnection(); st = conn.prepareStatement( "insertÃintoÃaccountsÃ(id,nomeProp,bilancio) values(?,?,?)"); st.setString(2, nomeProp); st.setDouble(3, bilancio); st.setString(1, accountID); st.executeUpdate(); return new AccountPK(accountID); } catch (Exception er) { throw new CreateException(er.toString()); } finally { try { if (st != null) st.close(); } catch (Exception er) { er.printStackTrace(); } try { if (conn != null) conn.close(); } catch (Exception er) { er.printStackTrace(); } } } Listato 5.31: AccountBean.java 5.5 Entity Bean 163 //metodi find public AccountPK ejbFindByPrimaryKey(AccountPK key) throws FinderException { PreparedStatement st = null; Connection conn = null; try { System.out.println("chiamatoÃilÃmetodoÃper la ricerca della chiave"); conn = getConnection(); st = conn.prepareStatement( "selectÃidÃfromÃaccountsÃwhereÃidÃ=Ã?"); st.setString(1, key.toString()); ResultSet rs = st.executeQuery(); rs.next(); return key; } catch (Exception er) { throw new FinderException(er.toString()); } finally { try { if (st != null) st.close(); } catch (Exception er) { er.printStackTrace(); } try { if (conn != null) conn.close(); } catch (Exception er) { er.printStackTrace(); } } } Listato 5.32: AccountBean.java 5.5 Entity Bean 164 public Collection ejbFindName(String name) throws FinderException { PreparedStatement st = null; Connection conn = null; Vector v = new Vector(); try { System.out.println("chiamatoÃilÃmetodoÃperÃlaÃricerca del nome del proprietario del cont"); conn = getConnection(); st = conn.prepareStatement( "selectÃidÃfromÃaccountsÃwhereÃnomePropÃ=Ã?"); st.setString(1, name); ResultSet rs = st.executeQuery(); while(rs.next()) { String id = rs.getString("id"); v.addElement(new AccountPK(id)); } return v; } catch (Exception er) { throw new FinderException(er.toString()); } finally { try { if (st != null) st.close(); } catch (Exception er) { er.printStackTrace(); } try { if (conn != null) conn.close(); } catch (Exception er) { er.printStackTrace(); } } } Listato 5.33: AccountBean.java 5.5 Entity Bean 165 package entityBean; public class AccountException extends Exception { public AccountException() { super(); } public AccountException(Exception e) { super(e.toString()); } public AccountException(String e) { super(e); } } Listato 5.34: AccountException.java come per esempio il prelievo di un quantitativo non presente sul conto. Inoltre distingue tra i problemi relativi all’account e altri problemi relativi al malfunzionamento del programma. Il deployment descriptor e jboss.xml Infine per completare il bean da installare su JBoss c’è bisogno degli ultimi due file XML rappresentati nei listati 5.35 e 5.36. Il file JBoss.xml non cambia rispetto agli esempi precedenti. Nel deployment descriptor ci sono alcuni nuovi tag: <persistence-type> Questo elemento indica il tipo di persistenza: BeanManaged o Container-Managed. <prim-key-class> Specifica il nome della Primary Key class <reentrant> Questo tag indica se il bean può chiamare se stesso attraverso un altro bean. Cioè, dati due bean A e B, sussiste il reentrant se, il bean A chiama il B, il quale richiama il bean A. <resource-ref> Questo elemento specifica i driver JDBC. 5.5 Entity Bean 166 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD Enterprise JavaBeans 2.0//EN’ ’http://java.sun.com/dtd/ejb-jar_2_0.dtd’> <ejb-jar> <enterprise-beans> <entity> <ejb-name>Account</ejb-name> <home>entityBean.AccountHome</home> <remote>entityBean.AccountRemote</remote> <ejb-class>entityBean.AccountBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>entityBean.AccountPK</prim-key-class> <reentrant>False</reentrant> <resource-ref> <res-ref-name>jdbc/ejbPool</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </entity> </enterprise-beans> </ejb-jar> Listato 5.35: Deployment descriptor <?xml version="1.0" encoding="UTF-8"?> <jboss> <enterprise-beans> <session> <ejb-name>Account</ejb-name> <jndi-name>Bank</jndi-name> </session> </enterprise-beans> </jboss> Listato 5.36: JBoss.xml 5.5 Entity Bean 167 Setup del database Hypersonic Ultimo passo, prima di poter installare il bean e poter usarlo, c’è bisogno di creare la tabella sul database. Per abilitare il database interno di JBoss bisogna modificare il file hsqldb-ds.xml che si trova nella cartella {dir.inst} \server\default\deploy. All’interno di questo file XML, sono presenti tre tag connection-url, due di questi sono commentati. Per abilitare il database si deve commentare il tag esistente e selezionare il tag <connectionurl>jdbc:hsqldb:hsql://localhost:1701</connection-url>. Inoltre bisogna eliminare il commento dai seguenti tag: <mbean code="org.jboss.jdbc.HypersonicDatabase" name="jboss:service=Hypersonic"> <attribute name="Port">1701</attribute> <attribute name="Silent">true</attribute> <attribute name="Database">default</attribute> <attribute name="Trace">false</attribute> <attribute name="No_system_exit">true</attribute> </mbean> <mbean code="org.jboss.jdbc.HypersonicDatabase" name="jboss:service=Hypersonic,database=localDB"> <attribute name="Database">localDB</attribute> <attribute name="InProcessMode">true</attribute> </mbean> Una volta salvato questo file, nella pagina iniziale di JBoss raggiungibile attraverso la pagina http://localhost:8080/, all’interno della JMX Console cliccare sul servizio: service=Hypersonic nella sezione jboss. All’interno di questa pagina, il metodo void startDatabaseManager() apre il programma per la gestione del database Hypersonic. Con questo programma è possibile, attraverso il linguaggio SQL, creare le tabelle per gli Entity Bean. Questo passaggio è automatico per i Container-Managed Bean. Per questo esempio c’è bisogno della tabella accounts creata con il seguente codice sql: CREATE TABLE accounts ( id varchar(64), nomeProp varchar(64), bilancio numeric(18) ) Ora il bean è pronto per essere usato. 5.5 Entity Bean 168 Il Client Nei Listati 5.37 e 5.38 è proposta una semplice classe Java che effettua alcune operazioni bancarie come un deposito e un prelievo. Per evitare di far rimanere dati inutili sul database è stato creato un blocco try-catch allo scopo di eliminare il bean in qualunque caso. 5.5.4 Creazione di un Entity Bean Container-Managed persistent Nel caso di un EJB Container-Managed Persistent (CMP), non c’è bisogno di implementare alcuna linea di codice relativa alla gestione dei dati, è l’EJB Container che fornisce le operazioni di salvataggio. Un altro vantaggio di questo modello e che c’è una separazione tra un entity bean e la sua rappresentazione statica. Questa separazione comporta una maggiore facilità nel modificare la rappresentazione del bean in quanto questa descrizione non si trova nel codice. Per ottenere quest’obiettivo, i sorgenti devono poter esser privi di qualunque codice JDBC o altro codice per la manipolazione dei dati. Il codice per la manipolazione dei dati viene generato dal container creando una sotto classe della classe entity bean. In questo modo non c’è bisogno di scrivere i campi da salvare in quanto sono generati dal container nella sotto classe. L’assenza di questi campi comporta l’assenza dei metodi get e set nella classe enterprise bean. Anche questi metodi sono generati dal container, per sapere la nomenclatura di questi metodi bisogna creare la classe enterprise bean come una classe astratta, dove i metodi get e set sono solo dichiarati. Il Container conosce il nome di questi campi dal deployment descriptor. I nomi di questi campi, delimitati dal tag <cmp-field>, devono avere la stessa nomenclatura dei metodi get e set: per esempio data una variabile prezzoBase il nome dei metodi get e set dovrà essere getPrezzoBase() e setPrezzoBase(). Il passo successivo per la completa comprensione del modello CMP è sapere come il Container crea i metodi finder. Per scrivere il codice in maniera indipendente dal Container specifico in modo da mantenere la proprietà fondamentale degli EJB si utilizza il linguaggio EJB Query Language (EJB-QL). Con questo linguaggio object-oriented molto simile a SQL, il container è capace di generare il codice necessario per l’accesso e il salvataggio dei dati. L’esempio seguente mostra un entity bean Container-Managed Persistent. 5.5 Entity Bean 169 package clientBean; import import import import entityBean.*; javax.naming.*; javax.rmi.*; java.util.*; public class Index { public static void main(String[] args) { AccountRemote account = null; try { Properties prop = new Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces"); prop.put(Context.PROVIDER_URL, "localhost"); Context ctx = new InitialContext(prop); Object obj = ctx.lookup("Bank"); AccountHome home = (AccountHome) javax.rmi.PortableRemoteObject. narrow(obj, AccountHome.class); System.out.println("IlÃtotaleÃdegliÃaccountÃinizialeÃin bancaè : "+home.getTotalBankValue()); home.create("121320-456-7890","JonhÃSmith"); Iterator i = home.findName("JonhÃSmith").iterator(); if(i.hasNext()) { account = (AccountRemote) javax.rmi. PortableRemoteObject.narrow(i.next(), AccountRemote.class); } else throw new Exception("AccountÃnonÃtrovato"); System.out.println("BilancioÃinizialeÃ= "+account.getBilancio()); Listato 5.37: Il client Index.java 5.5 Entity Bean 170 account.deposit(100); System.out.println("DopoÃaverÃdepositatoÃiÃsoldiÃil uovo bilancioè : "+account.getBilancio()); AccountPK pk = (AccountPK) account.getPrimaryKey(); account = null; account = home.findByPrimaryKey(pk); System.out.println("trovatoÃaccountÃconÃilÃseguente id: "+pk+". Il suo bilancioè di : "+ account.getBilancio()); System.out.println("OraÃprovoÃaÃprelevareÃ150"); account.prelievo(150); } catch(Exception er) { System.out.println("l’eccezioneèÃÃstata lanciata...: "+er.toString()); er.printStackTrace(); } finally { try { System.out.println("distruzioneÃdelÃbean"); account.remove(); } catch(Exception e) { e.printStackTrace(); } } } } Listato 5.38: Il client Index.java 5.5 Entity Bean 171 package entityBean; import javax.ejb.*; import java.rmi.RemoteException; public interface Product extends EJBObject { public String getNome() throws RemoteException; public void setNome(String unNome) throws RemoteException; public String getDescrizione() throws RemoteException; public void setDescrizione(String unaDescrizione) throws RemoteException; public double getPrezzoBase() throws RemoteException; public void setPrezzoBase(double unPrezzo) throws RemoteException; public String getProdotto() throws RemoteException; } Listato 5.39: Product.java La remote interface: Product.java E’ l’interfaccia che il client invoca, essa è mostrata nel Listato 5.39. La home interface: ProductHome.java Questa classe è mostrata nel Listato 5.43. Qui sono esposti i metodi per la creazione e la ricerca degli entity bean. La primary key class: ProductPK.java Come per il modello BMP, anche il Container-Manager Persistent ha bisogno della classe che identifica la chiave primaria. La classe è mostrata nel Listato 5.41. 5.5 Entity Bean 172 package entityBean; import java.rmi.RemoteException; import java.util.Collection; import javax.ejb.*; public interface ProductHome extends EJBHome{ public Product create(String prodotto,String name, String descrizione,double prezzo) throws CreateException, RemoteException; public Product findByPrimaryKey(ProductPK key) throws FinderException, RemoteException; public Collection findNome(String unNome) throws FinderException, RemoteException; public Collection findDescrizione(String unaDescrizione) throws FinderException, RemoteException; public Collection findPrezzoBase(double unPrezzo) throws FinderException, RemoteException; public Collection findProdottiCostosi(double unPrezzo) throws FinderException, RemoteException; public Collection findProdottiEconomici(double unPrezzo) throws FinderException, RemoteException; public Collection findAll() throws FinderException, RemoteException; } Listato 5.40: ProductHome.java 5.5 Entity Bean 173 package entityBean; import java.io.Serializable; public class ProductPK implements Serializable{ public String prodotto; public ProductPK(String ID) { prodotto = ID; } public ProductPK() { } public int hashCode() { return prodotto.hashCode(); } public String toString() { return prodotto; } public boolean equals(Object obj) { if(obj instanceof ProductPK) { if(((ProductPK)obj).prodotto.equals(prodotto)) return true; else return false; } else return false; } } Listato 5.41: ProductPK.java 5.5 Entity Bean 174 La enterprise bean class: ProductBean.java Nel Listato 5.42 è mostrata l’implementazione dell’entity bean containermanaged persistent. Come si nota la dimensione del codice da scrivere è notevolmente diminuita. Questo diminuisce la possibilità di bug nel codice e aumenta la facilità di manutenzione. Non sono utilizzati campi quindi il metodo ejbCreate() utilizza i metodi set per popolare il database. Il deployment descriptor Il deployment descriptor in questo modello diventa fondamentale. Viene usato dal container per conoscere i campi del bean e come realizzare il metodo finder. Il codice per questo esempio è mostrato nei Listati dal 5.44 al 5.47. La prima parte è la stessa che caratterizza anche il modello BMP. Successivamente c’è la definizione dei campi del bean, che devono essere gli stessi dei metodi get/set. Infine le query per i metodi find. jboss.xml e jbosscmp-jdbc.xml Questi file servono a JBoss per inizializzare il database e sapere come abbinare ai campi del bean la corrispettiva colonna del database. Nel Listato 5.48 è mostrato il file jBoss.xml mentre nel Listato 5.49 è mostrato il file jbosscmp-jdbc.xml. Quest’ultimo file gestisce il database nel tag </defaults>, mentre nel tag </enterprise-beans> gestisce i campi della tabella. I tag <datasource> e <datasource-mapping> sono specifichi per JBOSS e identificano il database Hypersonic. Il tag <create-table> indica se al momento del deploy del bean il container deve creare la cartella. Il tag <remove-table> viene usato dal container per conoscere se alla rimozione del bean deve rimuovere anche la tabella. Il tag <pk-constraint> indica il bisogno di creare una chiave primaria per questa tabella Per le colonne del database i tag <field-name> e <column-name> specificano l’associazione tra colonna e campo del bean. field-name è lo stesso campo che si trova nel deployment descriptor, column-name è il campo utilizzato nelle query per i metodi finder. Il client: Index.java Questa è una semplice classe Java che ha lo scopo di testare il bean. Normalmente non è il client ad effettuare queste operazioni ma i session bean che interagiscono con gli entity bean. Il codice client è mostrato nel Listato 5.49. 5.5 Entity Bean 175 package entityBean; import javax.ejb.*; public abstract class ProductBean implements EntityBean{ private EntityContext ctx; public public public public public public public public abstract abstract abstract abstract abstract abstract abstract abstract String getNome(); void setNome(String unNome); String getDescrizione(); void setDescrizione(String unaDescrizione); double getPrezzoBase(); void setPrezzoBase(double unPrezzo); String getProdotto(); void setProdotto(String unID); public void ejbActivate() { System.out.println("BeanÃattivato"); } public void ejbPassivate() { System.out.println("BeanÃpassivato"); } public void ejbRemove() { System.out.println("BeanÃrimosso"); } public void ejbLoad() { System.out.println("BeanÃcaricato"); } Listato 5.42: ProductBean.java 5.5 Entity Bean 176 public void ejbStore() { System.out.println("BeanÃsalvato"); } public void setEntityContext(EntityContext unCtx) { System.out.println("SettatoÃentityÃcontext"); ctx = unCtx; } public void ejbPostCreate(String prodotto,String name, String descrizione,double prezzo) { System.out.println("ChiamatoÃilÃpostÃcreate"); } public ProductPK ejbCreate(String prodotto,String name, String descrizione,double prezzo) throws CreateException { System.out.println("beanÃcreatoÃ"); setProdotto(prodotto); setNome(name); setDescrizione(descrizione); setPrezzoBase(prezzo); ProductPK pk = new ProductPK(prodotto); return pk; } public void unsetEntityContext(){ctx = null;} } Listato 5.43: ProductBean.java 5.5 Entity Bean 177 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD Enterprise JavaBeans 2.0//EN’ ’http://java.sun.com/dtd/ejb-jar_2_0.dtd’> <ejb-jar> <enterprise-beans> <entity> <ejb-name>ProductsBean</ejb-name> <home>entityBean.ProductHome</home> <remote>entityBean.Product</remote> <ejb-class>entityBean.ProductBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>entityBean.ProductPK</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>products</abstract-schema-name> <cmp-field> <field-name>prodotto</field-name> </cmp-field> <cmp-field> <field-name>nome</field-name> </cmp-field> <cmp-field> <field-name>descrizione</field-name> </cmp-field> <cmp-field> <field-name>prezzoBase</field-name> </cmp-field> Listato 5.44: ejb-jar.XML 5.5 Entity Bean 178 <query> <query-method> <method-name>findNome</method-name> <method-params> <method-param>java.lang.String</method-param> </method-params> </query-method> <ejb-ql> <![CDATA[select object(a) from products as a where a.nome = ?1]]> </ejb-ql> </query> <query> <query-method> <method-name>findDescrizione</method-name> <method-params> <method-param>java.lang.String</method-param> </method-params> </query-method> <ejb-ql> <![CDATA[select object(a) from products as a where a.descrizione = ?1]]> </ejb-ql> </query> <query> <query-method> <method-name>findPrezzoBase</method-name> <method-params> <method-param>double</method-param> </method-params> </query-method> Listato 5.45: Product.java 5.5 Entity Bean 179 <ejb-ql> <![CDATA[select object(a) from products as a where a.prezzoBase = ?1 ]]> </ejb-ql> </query> <query> <query-method> <method-name>findProdottiCostosi</method-name> <method-params> <method-param>double</method-param> </method-params> </query-method> <ejb-ql> <![CDATA[select object(a) from products as a where a.prezzoBase > ?1]]> </ejb-ql> </query> <query> <query-method> <method-name>findProdottiEconomici</method-name> <method-params> <method-param>double</method-param> </method-params> </query-method> <ejb-ql> <![CDATA[select object(a) from products as a where a.prezzoBase < ?1]]> </ejb-ql> </query> Listato 5.46: Product.java 5.5 Entity Bean 180 <query> <query-method> <method-name>findAll</method-name> <method-params> </method-params> </query-method> <ejb-ql> <![CDATA[select object(a) from products as a where a.prodotto is not null]]> </ejb-ql> </query> </entity> </enterprise-beans> </ejb-jar> Listato 5.47: Product.java <?xml version="1.0" encoding="UTF-8"?> <jboss> <enterprise-beans> <entity> <ejb-name>ProductsBean</ejb-name> <jndi-name>prodotti</jndi-name> </entity> </enterprise-beans> </jboss> Listato 5.48: jboss.xml 5.5 Entity Bean 181 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE jbosscmp-jdbc PUBLIC "-//JBoss//DTDÃJBOSSCMP-JDBCÃ3.0//EN" "http://www.jboss.org/j2ee/dtd/jbosscmp-jdbc_3_0.dtd"> <jbosscmp-jdbc> <defaults> <datasource>java:/DefaultDS</datasource> <datasource-mapping>Hypersonic SQL</datasource-mapping> <create-table>true</create-table> <remove-table>true</remove-table> <pk-constraint>false</pk-constraint> </defaults> <enterprise-beans> <entity> <ejb-name>ProductsBean</ejb-name> <table-name>products</table-name> <cmp-field> <field-name>nome</field-name> <column-name>nomeProd</column-name> </cmp-field> <cmp-field> <field-name>descrizione</field-name> <column-name>descrizioneProd</column-name> </cmp-field> <cmp-field> <field-name>prezzoBase</field-name> <column-name>prezzo</column-name> </cmp-field> <cmp-field> <field-name>prodotto</field-name> <column-name>prod</column-name> </cmp-field> </entity> </enterprise-beans> </jbosscmp-jdbc> Listato 5.49: jbosscmp-jdbc.xml 5.5 Entity Bean 182 package clientBean; import entityBean.*; import javax.naming.*; import javax.rmi.PortableRemoteObject; import java.util.*; public class Index { public static void main(String[] args){ ProductHome home = null; try { Properties prop = new Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces"); Context ctx = new InitialContext(prop); Object obj = ctx.lookup("prodotti"); home = (ProductHome) javax.rmi.PortableRemoteObject. narrow(obj, ProductHome.class); Product c = home.create("123","P5-200", "PentiumÃ2ÃGHz",100); home.create("124","P5-300", "PentiumÃ3ÃGHz",600); home.create("125","P5-350", "PentiumÃ3,5ÃGHz",1000); home.create("126","AMD-300", "AMDÃ3ÃGHz",1000); home.create("127","AMD-320", "AMDÃ3,2ÃGHz",1200); home.create("128","AMD-35", "AMDÃ3.5ÃGHz",1500); Iterator i = home.findNome("P5-300").iterator(); System.out.println("QuestiÃsonoÃiÃprodottoÃtrovati"); while(i.hasNext()) { Product prod = (Product) PortableRemoteObject. narrow(i.next(),Product.class); System.out.println(prod.getDescrizione()); } Listato 5.50: Il client 5.5 Entity Bean 183 System.out.println("ricercaÃtuttiÃiÃprodotti che costano 1000"); i = home.findPrezzoBase(1000).iterator(); while(i.hasNext()) { Product prod = (Product) PortableRemoteObject. narrow(i.next(),Product.class); System.out.println(prod.getDescrizione()); } } catch(Exception er) { er.printStackTrace(); } finally { try { if (home != null) { Iterator i = home.findAll().iterator(); while (i.hasNext()) { Product prod = (Product) PortableRemoteObject. narrow(i.next(), Product.class); System.out.println("rimozioneÃdelÃprodottoÃ"+ prod.getNome()); prod.remove(); } } } catch(Exception er) { er.printStackTrace(); } } } } Listato 5.51: Il client 5.6 Message-driven Bean 5.6 184 Message-driven Bean Un Message-Driven Bean (MDB) è un Bean in cui il concetto di interfaccia remota è del tutto assente, ed il solo meccanismo di interazione con il mondo esterno è quello basato sulla ricezione di messaggi JMS. Si tratta quindi di componenti stateless transaction aware, il cui unico compito è quello di processare messaggi JMS provenienti da topic o code. Il valore aggiunto che si ha nell’usare un MDB al posto di una normale applicazione client JMS è che in questo caso l’ascoltatore, essendo un Enterprise Bean, vive completamente all’interno di un contesto sicuro, transazionale e fault-tolerant. Un altro grosso vantaggio è che, rispetto ad un semplice client, il MDB è in grado di processare i messaggi in modo concorrente: lo sviluppatore si deve concentrare sullo sviluppo di un solo componente, ovvero sulla logica di processamento di un messaggio, mentre al resto pensa il Container che potrà eseguire i vari Bean in modo concorrente. Un MDB può ricevere centinaia di messaggi e processarli anche tutti nello stesso istante dato che il Container eseguirà più istanze dello stesso MDB gestite in modalità di pool. In modo del tutto analogo a quanto avviene per i Session o gli Entity, anche gli MDB sono gestiti in tutto il loro ciclo di vita e durante l’esecuzione dei metodi di business dal Container. Per questo, sebbene essi siano in grado di processare messaggi JMS, l’effettiva ricezione viene gestita dal container, che poi in modalità di callback inoltra la chiamata al Bean. 5.6.1 Utilizzo dei MDB Per chi si avvicina per la prima volta agli MDB uno dei dubbi più ricorrenti è su come e quando un MDB debba essere utilizzato. La modalità corretta di utilizzo di un MDB è anche la più semplice che si possa immaginare. Se in un contesto EJB gli Entity rappresentano entità astratte e i Session degli oggetti di servizio in cui eseguire metodi di business logic, gli MDB sono oggetti il cui solo scopo è quello di operare come intermediari per l’esecuzione della business logic in concomitanza di determinati eventi (ovvero all’arrivo di un messaggio). Per questo motivo un MDB dovrebbe implementare solamente la logica di gestione del messaggio e dell’analisi delle sue caratteristiche e dar vita ad un flusso di operazioni invocando la business logic contenuta nei metodi remoti di un session bean. Ad esempio un MDB potrebbe estrapolare i campi dall’header del messaggio e successivamente stabilire quale metodo di un Session invocare passandogli gli opportuni parametri. I MDB possono avere campi di istanza come i Session e sono mantenuti durante il loro ciclo di vita, ma essendo stateless e non potendo essere associati ad un client in 5.6 Message-driven Bean 185 Figura 5.8: Scema del ciclo vita di un message-driven bean particolare, devono essere gestiti dal container in modo autonomo secondo la logica del pool. Questi Bean sono i più semplici da implementare in quanto richiedono solo la enterprise bean class. L’unico modo di interazione con i client è attraverso i messaggi JMS, quindi non c’è bisogno delle interfacce home e remote con il client. Questo Bean deve implementare le interfacce sia con JMS (MessageListener) che con il container MessageDrivenBean). Quando viene installato un MDB sul container, su di esso vengono chiamati i metodi setMessageDrivenContext() e ejbCreate(). A questo punto, il MDB è pronto a ricevere messaggi. Dopo l’installazione per ogni messaggio ricevuto dal MDB, il metodo onMessage(Message) viene invocato. Il Container è responsabile di serializzare i messaggi: ogni MDB può processare al più un solo messaggio alla volta ma il container può avere copie dello stesso Bean permettendo cosı̀ l’accesso concorrenziale. Quando viene disinstallato il MDB viene invocato il metodo ejbRemove() . La Figura 5.8 mostra il ciclo vita di un MDB, molto simile ad un session bean stateless. Capitolo 6 Applicazione completa sugli EJB In tale capitolo verrà mostrata un’applicazione riassuntiva contenente Session Bean e Message Driven-Bean. Si riporterà ora in dettaglio il funzionamento di tale applicazione per meglio comprendere il contesto in cui si opera e le funzionalità che l’applicazione implementa. Successivamente si descriverà nei particolari la tecnologia utilizzata per la sua realizzazione. Per la creazione dell’intero applicativo ci si può riferire alla Figura 6.1 che mostra le classi dell’applicazione. Tutti i bean sono contenuti nella stessa applicazione: ciò significa che il deployment descriptor sarà uno solo e conterrà le informazioni di tutti i beans. Per maggiore chiarezza, ogni bean ha un proprio package, in questo modo è più semplice capire quali siano le classi relative ad ogni bean. Iniziamo nel descrivere l’applicazione. 6.1 Vendita di libri on-line Tale esempio riguarda una vendita di libri on-line. In particolare, la pagina iniziale che si presenta all’utente nel momento in cui si collega all’applicazione attraverso un browser riporta semplicemente gli strumenti necessari per effettuare il login, come si evince dalla Figura 6.2. Se i dati sono stati inseriti correttamente, si accede ad una seconda pagina in cui si notifica che il login è avvenuto con successo, nel caso in cui il nome utente e la password inseriti siano corretti; in caso contrario, la pagina conterrà un messaggio che avvisa del fallimento del login (di seguito è ripor- 186 6.1 Vendita di libri on-line Figura 6.1: Schema dell’applicazione Figura 6.2: Pagina iniziale 187 6.1 Vendita di libri on-line 188 Figura 6.3: Caso A: inserimento dei dati; Caso B: login avvenuto con successo; Caso C: login errato. tata una sequenza di immagini in cui si visualizzano i vari casi possibili del login) Figura 6.3. Una volta inserito il nome utente e la password in modo corretto, si giunge alla pagina principale dell’applicazione Figura 6.4 in cui è presente una lista dei libri disponibili dalla quale l’utente può scegliere quelli di interesse ed aggiungerli nel proprio carrello. La scelta avviene selezionando il libro scelto e cliccando sul bottone Add to. A questo punto, verrà visualizzata una nuova pagina attraverso la quale viene fornito il feedback all’utente: il libro è stato aggiunto correttamente nel carrello (Figura 6.5). A questo punto, cliccando su Continua si torna di nuovo nella pagina principale che riporta, in alto a sinistra, il carrello contenente tutti i libri che il cliente ha deciso di acquistare con il relativo conto. Il cliente può rimuovere dei libri dal proprio carrello selezionandoli e cliccando sul tasto Delete (anche in tal caso verrà visualizzata un’ulteriore pagina che dà conferma all’utente che il libro in questione è stato eliminato con successo dal carrello). Una volta che l’utente ha finito i propri acquisti può procedere al pagamento attraverso il tasto Procedi al pagamento. Si giunge cosı̀ ad un’altra pagina dell’applicazione nella quale vengono richiesti all’utente alcune informazioni per effettuare il pagamento (Figura 6.6) quali: nome, numero di carta di credito e tipo di 6.1 Vendita di libri on-line 189 Figura 6.4: Pagina principale Figura 6.5: Conferma del corretto inserimento del nuovo ordine nel carrello 6.1 Vendita di libri on-line 190 Figura 6.6: Richiesta dati per il pagamento Figura 6.7: Schermata riepilogativa dei dati per il pagamento carta di credito. Una volta inseriti tutti i dati, attraverso il bottone Send To viene caricata una pagina riepilogativa dei dati inseriti per il pagamento (Figura 6.7) in questa, una volta cliccato su Continua, si torna all’home page dove altri utenti potranno effettuare nuovi acquisti. L’applicazione consente al venditore di avere una gestione degli ordini automatizzata mediante un’interfaccia che gli elenca gli ordini dei clienti (Figura 6.8). Ciò avviene cliccando sul tasto start. Una volta che l’ordine è stato evaso, esso può essere rimosso selezionandolo e cliccando sul tasto Processato. Il tasto Chiudi consente invece di chiudere l’intera applicazione. 6.2 Alcuni Diagrammi UML dell’Applicazione 191 Figura 6.8: Ordini dei clienti 6.2 Alcuni Diagrammi UML dell’Applicazione Di seguito, mostriamo alcuni diagrammi UML dell’Applicazione. Use Case Consideriamo come scenario principale quello in cui tutto va a buon fine: Acquista un libro 1. Il cliente effettua il login 2. Il cliente scorre la lista e seleziona il libro da acquistare 3. Il cliente aggiunge il libro al carrello 4. Il cliente va alla cassa 5. Il cliente riempie i campi relativi alla carta di credito 6. Il sistema autorizza l’acquisto Una possibile variante allo scenario principale può essere la seguente: Autorizzazione Fallita al passo 6, il sistema boccia l’autorizzazione a rilevare il credito. Consente al cliente di reinserire le informazioni relative alla carta di credito e riprovare. In Figura 6.9 è mostrato il Diagramma dei Casi d’Uso della nostra Applicazione. Class Diagram In Figura 6.10 è rappresentato il diagramma delle classi relativo al componente Client Swing della nostra applicazione. Il dia- 6.2 Alcuni Diagrammi UML dell’Applicazione 192 0..* 1 Acquistare Libri BookOrder Costumer Figura 6.9: Diagramma Casi d’Uso PrincipaleListener Application main() : void OrderListener 1 START : String CHIUDI : String ELIMINA : String start() : void chiudi() : void elimina() : void 1 listModel : DefaultListModel connection : DefaultListModel onMessage(Message : void) : void stop() : void 1 Order 1 Principale lista : JList model : DefaultListModel startBtn : JButton chiudiBtn : JButton eliminaBtn : JButton sudPnl : JPanel listScrollPane : int user : String books : String getUser() : void getBooks() : void Figura 6.10: Diagramma delle Classi relativo a Client Swing 6.3 Package BookOrder 193 <<Form>> DeleteBook 1 1 <<submit>> <<Servlet>> DeleteBook index.html 1 1 <<Form>> Pagamento 1 <<Jsp>> Ordini 1 1 1 <<Form>> Login 1 1 <<redirect>> 1 1 <<submit>> <<Jsp>> Pagamento 1 1 1 <<Form>> Controller 1 1 <<submit>> <<submit>> 1 1 <<Form>> PagamentoServlet <<Servlet>> PagamentoServlet 1 <<submit>> 1 1 <<Servlet>> ConfirmServlet <<Servlet>> Controller <<submit>> 1 1 Figura 6.11: Diagramma delle Classi relativo ad Accesso Web gramma delle classi in Figura 6.11 è relativo alla parte web (componente Accesso web) ed è stato modellato secondo la metodologia descritta da Jim Conallen nel suo articolo [12] 6.3 Package BookOrder Il package BookOrder contiene al suo interno le seguenti classi: BookOrder: interfaccia remota che descrive i metodi implementati dal Bean per fornire il servizio di acquisto on line (quando un client vuole usare un’istanza dell’enterprise class, non invocherà mai direttamente l’istanza, piuttosto l’invocazione viene intercettata dal ContainerEJB che agisce come livello intermedio tra client e bean). BookOrderBean: contiene la logica del sistema (è un esempio tipico di sessionbean stateful). Tale bean tiene memoria dello stato di ogni client (carrello della spesa e costi) e gestisce la comunicazione JMS degli eventi associati all’acquisto di un libro. All’interno del metodo setSessionContext si realizza la comunicazione tra il Bean ed il Container. All’interno di tale classe sono contenuti i metodi di gestione dell’ejb, compresi quelli standard per la gestione degli ejb stateful (ejbActivate utilizzato per richiamare l’EJB dalla memoria di massa 6.4 Package BookOrderMDB 194 quando la conversazione con il client torna attiva, ejbPassivate che consente di memorizzare l’EJB su un archivio permamente per ridurre il consumo di memoria) e i metodi che implementano la logica di business vera e propria. BookOrderHome: classe che contiene il client che invoca l’EJBObject e non il bean. Questo avviene solo se il client ha il riferimento a questo oggetto. Il client ottiene il riferimento attraverso un produttore di EJBObject: l’HomeObject responsabile della creazione, della ricerca e della rimozione di un EJBObject. BookOrderHome è l’interfaccia che consente di inizializzare correttamente l’EJBObject. Il Listato 6.2 mostra la classe BookOrderHome.Java. Il vero e proprio bean viene mostrato nei Listati da 6.3 a 6.8. dove sono implementati tutti i metodi che sono disponibili nella remote interface. 6.4 Package BookOrderMDB Nel package BookOrderMDB c’è solo la classe BookMDBean contenente i metodi di gestione del bean tra cui il metodo onMessage(). Il bean riceve il messaggio con l’ordine che è stato eseguito dal client e lo scrive in maniera opportuna su un file di log C:\orders.log 6.5 Package Archivio La cartella RMIBookStore contiene il package Archivio. Al suo interno ci sono le seguenti classi: Book: contiene i metodi getTitolo, getListaAutori e getCosto. DataLayer: l’interfaccia remota, contenente i metodi di business dell’rmi server invocabili remotamente (è l’oggetto che consente la comunicazione remota tra il nostro client realizzato con l’EJB ed il server). DataLayerImpl: è l’implementazione dell’Interfaccia remota in una classe concreta che estende UnicastRemoteObject (facilita la creazione di oggetti remoti). Dentro tale classe viene definito univocamente l’elenco degli utenti che possono accedere ai nostri servizi con le rispettive password, quindi viene effettuato il controllo dell’identità degli utenti che tentano di accedere al sistema per comprare i libri e viene verificato che il codice della carta sia di 16 caratteri e che i caratteri siano numeri, dopodichè 6.5 Package Archivio package BookOrder; import javax.ejb.EJBObject; import java.util.*; import Archivio.*; public interface BookOrder extends EJBObject { boolean addBook(String name) throws java.rmi.RemoteException; boolean delBook(String name) throws java.rmi.RemoteException; Book[] getCarrello() throws java.rmi.RemoteException; float getConto() throws java.rmi.RemoteException; Book[] getAvailableBooks() throws java.rmi.RemoteException; boolean closeOrder(String numCarta,int tipo) throws java.rmi.RemoteException; int VISA = 0; int MASTERCARD = 1; int AMERICANEXPR = 2; //definizione del tipo di carta } } Listato 6.1: BookOrder.java package BookOrder; import javax.ejb.*; public interface BookOrderHome extends EJBHome { BookOrder create(String user,String pass) throws java.rmi.RemoteException, CreateException; } Listato 6.2: BookOrderHome.java 195 6.5 Package Archivio package BookOrder; import import import import import import javax.ejb.*; java.rmi.*; Archivio.*; java.util.*; javax.jms.*; javax.naming.*; public class BookOrderBean implements SessionBean { private SessionContext ctx; private DataLayer istanza; private String user; private Vector carrello; private float conto; private javax.jms.Topic destination; private TopicConnection connection; //comunicazione jms degli eventi associati all’acquisto //di un libro public BookOrderBean() { carrello=new Vector(); conto=0; destination = null; connection = null; } public void setSessionContext(SessionContext parm1) throws EJBException, RemoteException //realizza la comunicazione tra il Bean ed il Container //il parametro viene utilizzato dal MDB { ctx=parm1; } Listato 6.3: BookOrderBean.java 196 6.5 Package Archivio //********* //metodi di gestione degli EJB //********* public void ejbCreate(String utente,String passwd) throws RemoteException, CreateException { System.out.println("BookOrderBean:Create()"); try { istanza=(DataLayer)Naming.lookup ("rmi://localhost:2000/Archivio"); if (istanza.checkUser(utente,passwd)) { user=utente; createJMSConnection(); } else throw(new CreateException()); } catch(JMSException jme) { jme.printStackTrace(); throw(new CreateException()); } catch(NamingException nme) { nme.printStackTrace(); throw(new CreateException()); } catch(java.net.MalformedURLException mue) { mue.printStackTrace(); throw(new CreateException()); } catch(NotBoundException nbe) { nbe.printStackTrace(); throw(new CreateException()); } } Listato 6.4: BookOrderBean.java 197 6.5 Package Archivio 198 public void ejbRemove() throws EJBException, RemoteException { try { System.out.println("BookOrderBean:Remove()"); connection.close(); } catch(JMSException err) { err.printStackTrace(); } } //metodi standard per la gestione dell’EJB stateful public void ejbActivate() throws EJBException, RemoteException { } public void ejbPassivate() throws EJBException, RemoteException { } //************** //metodi che implementano la logica di business vera e propria //************** public boolean addBook(String name) throws RemoteException { Book newBook=istanza.getBookByName(name); if (carrello.indexOf(newBook)==-1) //se il libro nonè nel carrello { carrello.add(newBook); conto+=newBook.getCosto(); return(true); } else return(false); } Listato 6.5: BookOrderBean.java 6.5 Package Archivio public boolean delBook(String name) throws RemoteException { Iterator iter=carrello.iterator(); while(iter.hasNext()) { Book aBook=(Book)iter.next(); if (aBook.getTitolo().equals(name)) { iter.remove(); conto-=aBook.getCosto(); return(true); } } return(false); } public Book[] getCarrello() throws RemoteException //restituisce i libri che si trovano nel carrello { Book[] retValue=new Book[carrello.size()]; Iterator iter=carrello.iterator(); int i=0; while(iter.hasNext()) { retValue[i++]=(Book)iter.next(); } return(retValue); } Listato 6.6: BookOrderBean.java 199 6.5 Package Archivio public float getConto() throws RemoteException { return(conto); } public Book[] getAvailableBooks() throws RemoteException { Book list[]=istanza.getList(); int size=list.length-carrello.size(); //numero di libri rimasti ancora non acquistati Book retValue[]=new Book[size]; int j=0; for(int i=0;i<list.length;i++) { if (carrello.indexOf(list[i])<0) //verifica sei i libri sono nel carrello o meno retValue[j++]=list[i]; //se non ci sono aggiorna l’array dei libri //ancora disponibili } return(retValue); } Listato 6.7: BookOrderBean.java 200 6.5 Package Archivio 201 public boolean closeOrder(String numCarta, int tipo) throws RemoteException { try { if(!istanza.checkCreditCard(user,numCarta,tipo)) //se la cartaè sbagliata non chiude l’ordine return(false); TopicSession session=connection.createTopicSession (false,Session.AUTO_ACKNOWLEDGE); TopicPublisher publisher = session.createPublisher(destination); ObjectMessage message=session.createObjectMessage(); message.setObject(new Order(user,carrello)); publisher.send(message); return(true); } catch(JMSException err) { err.printStackTrace(); return(false); } } private void createJMSConnection() throws NamingException,JMSException { Context ctx = new InitialContext(); TopicConnectionFactory cf = (TopicConnectionFactory)ctx. lookup("ConnectionFactory"); connection = cf.createTopicConnection(); destination = (javax.jms.Topic)ctx.lookup("topic/ordiniTopic"); } Listato 6.8: BookOrderBean.java 6.5 Package Archivio package BookOrderMDB; import import import import javax.jms.*; javax.ejb.*; java.io.*; Archivio.*; public class BookMDBean implements MessageListener, MessageDrivenBean { private MessageDrivenContext ctx; private static final String nomeFileLog="/orders.log"; public void setMessageDrivenContext (MessageDrivenContext parm1) throws EJBException { ctx=parm1; } public void ejbCreate() { } public void ejbRemove() throws EJBException { System.out.println("BookMDBean:remove()"); } Listato 6.9: BookMDBean.java 202 6.5 Package Archivio 203 public void onMessage(Message m) { if (m instanceof ObjectMessage) { try { ObjectMessage om=(ObjectMessage)m; Order unOrdine=(Order)om.getObject(); FileWriter fw=new FileWriter(nomeFileLog,true); PrintWriter pw=new PrintWriter(fw,true); pw.print(unOrdine.getUser()); pw.print(":"); String[] books=unOrdine.getBooks(); if (books.length==0) return; pw.print("Ã"+books[0]); for(int i=1;i<books.length;i++) pw.print(",Ã"+books[i]); pw.println(); fw.close(); } catch(JMSException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } } } Listato 6.10: BookMDBean.java 6.6 Package BookStoreSwing 204 definisce il “magazzino”, cioè i libri che abbiamo a disposizione per gli utenti. Tale classe restituisce le caratteristiche di un libro il cui nome è dato da bookName. DataLayerReg: in tale classe si abilita il collegamento tra server e client RMI, l’applicazione instanzia un oggetto remoto e lo registra tramite un bind all’interno dell’rmi registry. 6.6 Package BookStoreSwing Il package BookStoreSwing contiene al suo interno le seguenti classi Application: è la classe contenente il metodo main, al cui interno si inizializza una nuova istanza della classe Principale. E’ da tale classe che parte l’esecuzione di tutta l’applicazione. Principale: classe che estende JFrame, cioè eredita tutte le proprietà di JFrame. Non contiene metodi, ma all’interno del suo costruttore ha sia la creazione di nuove istanze dei diversi componenti che andranno a comporre l’interfaccia principale che visualizzerà l’utente nell’utilizzo dell’applicazione, sia l’implemantazione della gestione degli eventi, richiamando l’ascoltatore definito nella classe PrincipaleListener. PrincipaleListener: tale classe implementa gli ActionListener, cioè definisce le funzioni dichiarate nell’interfaccia ActionListener. Nel metodo actionPerformed di tale classe vengono definite le azioni che saranno eseguite dal sistema a seconda dell’evento catturato. Tali azioni sono definite richiamando i metodi start(), chiudi() ed elimina() definiti sempre all’interno di tale classe. OrderListener: in questa classe si definisce una comunicazione JMS attraverso un topic (cioè attraverso un canale virtuale), tramite il metodo onMessage si definisce il formato dei messaggi da ricevere. 6.7 Modulo Accesso.war Nel progetto è inoltre presente il modulo Accesso.war. Tale modulo implementa la view, cioè sono state aggiunte delle servlet e delle JSP per permettere agli utenti di visionare ciò che si ottiene dal “dialogo” con i Beans. Le classi appartenenti a questo modulo sono: 6.7 Modulo Accesso.war package Archivio; public class Book implements java.io.Serializable { private String titolo; private String[] listaAutori; private float costo; public Book(String unTitolo, String[] autori, float unCosto) { titolo=unTitolo; listaAutori=autori; costo=unCosto; } public String getTitolo() { return(titolo); } public String[] getListaAutori() { return(listaAutori); } public float getCosto() { return(costo); } public boolean equals(Object unAltro) { if (!(unAltro instanceof Book)) return(false); return(((Book)unAltro).titolo.equals(titolo)); } } Listato 6.11: Book.java 205 6.7 Modulo Accesso.war 206 package Archivio; import java.rmi.*; public interface DataLayer extends Remote { boolean checkUser(String nome, String passwd) throws RemoteException; Book[] getList() throws RemoteException; boolean checkCreditCard (String nome, String numCarta, int tipo) throws RemoteException; Book getBookByName(String bookName) throws RemoteException; } Listato 6.12: DataLayer.java Order.jsp In tale JSP viene realizzata la gestione delle ordinazioni. Tale gestione comprende la possibilità di ottenere diversi servizi come la lista dei libri ordinati ( contenuta in carrello), la possibilità di eliminare degli ordini, etc. Come noto, una JSP è una pagina web contente sia codice HTML che codice Java, il quale viene compilato solo al momento della prima invocazione della pagina JSP generando una Servlet. In questo file abbiamo principalmente codice HTML, mentre le richieste Java e le chiamate ai Java Beans sono contenute in appositi tag. Pagamento.jsp In questa pagina avviene la gestione dei pagamenti. All’utente si presenta una form contenente la richiesta di diverse informazioni relative alla sua identità ed alla carta di credito utilizzata per i pagamenti. Nell’esempio considerato l’identità è “cablata” nel codice ( sono abilitati solo alcuni utenti). Controller In tale classe viene implementata una Servlet. Controller estende HttpServlet, cioè eredita le proprietà dell’interfaccia HttpServlet. In questa classe si ha solo l’implementazione del metodo service() il quale realizza la comunicazione bidirezionale con i client tramite i suoi due parametri: HttpServletRequest che contiene i dati inviati dal client, e HttpServletResponse che rappresenta la risposta della Servlet al client. All’interno di service viene dapprima richiamata la sessione, 6.7 Modulo Accesso.war package Archivio; import java.rmi.server.*; import java.rmi.*; public class DataLayerImpl extends UnicastRemoteObject implements DataLayer { private static String[][] userList= { { "deÃLeoni", "Max" }, { "Russo", "Ruggero" }, { "Mecella", "One" }, { "Topolino", "313" }, { "Paperino", "Qui" } }; private static Book[] bookList=new Book[5]; public DataLayerImpl() throws RemoteException { super(); String[] autori1={"deÃLeoni","Russo","Mecella"}; bookList[0]=new Book("J2EE",autori1,100); String[] autori2={"e","CayÃHorstmann"}; bookList[1]=new Book("ConcettiÃdiÃinformaticaÃe Fondamenti di Java 2",autori2,40); String[] autori3={"P.ÃYao","D.ÃDurant"}; bookList[2]=new Book(".NETÃCompactÃProgramming", autori3,50); String[] autori4={"AA.VV."}; bookList[3]=new Book("XYZ",autori4,2); String[] autori5={"S.ÃAmbler","T.ÃJewell"}; bookList[4]=new Book("MasteringÃEJB",autori5,77); } Listato 6.13: DataLayerImpl.java 207 6.7 Modulo Accesso.war public boolean checkUser(String nome, String passwd) throws RemoteException { for(int i=0;i<userList.length;i++) if(userList[i][0].equals(nome)) return(userList[i][1].equals(passwd)); return(false); } public Book[] getList() throws RemoteException { return(bookList); } public boolean checkCreditCard (String nome, String numCarta, int tipo) throws RemoteException { if (numCarta.length()!=16) return(false); char array[]=numCarta.toCharArray(); for (int i=0;i<array.length;i++) if (array[i]<’0’ || array[i]>’9’) return(false); return(true); } public Book getBookByName(String bookName) throws RemoteException { for(int i=0;i<userList.length;i++) if(bookList[i].getTitolo().equals(bookName)) return(bookList[i]); return(null); } } Listato 6.14: DataLayerImpl.java 208 6.7 Modulo Accesso.war package Archivio; import java.rmi.*; public class DataLayerReg public static void main(String[] args) throws Exception { DataLayerImpl obj=new DataLayerImpl(); Naming.rebind("rmi://localhost:2000/Archivio",obj); // NUMERO DI PORTA! } Listato 6.15: DataLayerReg.java public class Application { public static void main(String[] args) { new Principale(); } } Listato 6.16: Application.java 209 6.7 Modulo Accesso.war import javax.swing.*; import java.awt.*; import Archivio.*; public class Principale extends JFrame { public JList lista; public DefaultListModel model; private JPanel sudPnl=new JPanel(); private JScrollPane listScrollPane; public JButton startBtn=new JButton("Start"); public JButton chiudiBtn=new JButton("Chiudi"); public JButton eliminaBtn=new JButton("Processato"); private PrincipaleListener ascoltatore; public Principale() { super("ClientÃordini"); model=new DefaultListModel(); lista = new JList(model); listScrollPane = new JScrollPane(lista); sudPnl.add(startBtn); ascoltatore=new PrincipaleListener(this); sudPnl.add(eliminaBtn); sudPnl.add(chiudiBtn); startBtn.addActionListener(ascoltatore); chiudiBtn.addActionListener(ascoltatore); eliminaBtn.addActionListener(ascoltatore); startBtn.setActionCommand(ascoltatore.START); chiudiBtn.setActionCommand(ascoltatore.CHIUDI); eliminaBtn.setActionCommand(ascoltatore.ELIMINA); getContentPane().add (listScrollPane,BorderLayout.CENTER); getContentPane().add(sudPnl,BorderLayout.SOUTH); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); chiudiBtn.setEnabled(false); eliminaBtn.setEnabled(false); setSize(300,300); setVisible(true); } } Listato 6.17: Principale.java 210 6.7 Modulo Accesso.war import java.awt.event.*; import javax.swing.*; import Archivio.*; import javax.jms.JMSException; importjavax.naming.NamingException; public class PrincipaleListener implements ActionListener { private Principale frame; public static final String START="S"; public static final String CHIUDI="C"; public static final String ELIMINA="E"; private OrderListener jmsListener=null; public PrincipaleListener(Principale unFrame) { frame=unFrame; } public void actionPerformed(ActionEvent e) { try { String com=e.getActionCommand(); if (com == START) start(); else if (com == CHIUDI) chiudi(); else if (com == ELIMINA) elimina(); } catch(Exception err) { JOptionPane.showMessageDialog (frame,err.getMessage(),"LanciataÃeccez.", JOptionPane.ERROR_MESSAGE); err.printStackTrace(); } } Listato 6.18: PrincipaleListener.java 211 6.7 Modulo Accesso.war 212 private void start() throws NamingException,JMSException { jmsListener=new OrderListener (frame.model,"topic/ordiniTopic"); frame.startBtn.setEnabled(false); frame.chiudiBtn.setEnabled(true); frame.eliminaBtn.setEnabled(true); } private void chiudi() throws JMSException { jmsListener.stop(); frame.dispose(); } private void elimina() { /*Restituisce l’indice dell’elemento selezionato (-1 se nessun indiceè stato selezionato)*/ int index=frame.lista.getSelectedIndex(); if (index==-1) /*Se nonè stato selezionato alcun indice, allora errore, altrimenti lo visualizza*/ { JOptionPane.showMessageDialog (frame,"NessunÃindiceÃselezionato","Errore", JOptionPane.ERROR_MESSAGE); } else frame.model.remove(index); } } Listato 6.19: PrincipaleListener.java 6.7 Modulo Accesso.war import import import import import javax.jms.*; javax.naming.*; javax.swing.*; java.util.Properties; Archivio.*; public class OrderListener implements MessageListener { private DefaultListModel listModel; private TopicConnection connection = null; public OrderListener (DefaultListModel model, String topicName) throws NamingException, JMSException { listModel = model; InitialContext ctx = null; TopicConnectionFactory cf = null; TopicSession session = null; Topic destination = null; TopicSubscriber subscriber = null; Properties properties = new Properties(); properties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); properties.put (Context.URL_PKG_PREFIXES, "org.jnp.interfaces"); properties.put(Context.PROVIDER_URL, "localhost"); ctx = new InitialContext(properties); cf = (TopicConnectionFactory)ctx. lookup("ConnectionFactory"); destination = (Topic)ctx.lookup(topicName); connection = cf.createTopicConnection(); session = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); subscriber = session.createSubscriber(destination); subscriber.setMessageListener (this); connection.start(); } Listato 6.20: OrderListener.java 213 6.7 Modulo Accesso.war public void onMessage(Message mex) { try { if (mex instanceof ObjectMessage) { ObjectMessage objMex=(ObjectMessage)mex; Object obj=objMex.getObject(); if (obj instanceof Order) { Order ordine=(Order)obj; String text=ordine.getUser()+":"; String lista[]=ordine.getBooks(); for(int i=0;i<lista.length;i++) { text+="Ã"+lista[i]; } listModel.addElement(text); } } } catch(JMSException jmsExc) { listModel.addElement ("Errore:Ã"+jmsExc.getMessage()); } } public void stop() throws JMSException { connection.close(); } } Listato 6.21: OrderListener.java 214 6.8 Il deployment descriptor 215 dopodichè viene aggiunto un nuovo elemento al carrello virtuale di spesa dell’utente. PagamentoServlet Anche in tale classe si ha l’implementazione di una Servlet e quindi l’implementazione del metodo service(). In questa Servlet prima di tutto viene richiamata la sessione, dopodichè si verifica la tipologia di pagamento e se è avvenuto con successo. ConfirmServlet In tale classe viene implementata la Servlet necessaria alla verifica dei dati inseriti dall’utente per fare il login. Se il login ha avuto esito positivo allora viene inizializzato il contesto JNDI. DeleteBook L’implementazione di tale Servlet serve per eliminare degli elementi dal carrello di spesa dell’utente. Anche in tal caso abbiamo l’implementazione del metodo service(), nel quale come prima cosa appare la chiamata alla sessione. 6.8 Il deployment descriptor Essendo un singolo EJB composto da diversi bean, i deployment descriptor sono uniti in un singolo file. L’ejb-jar file di questa applicazione è mostrato nel Listato 6.34. Il file specifico per JBoss è jboss.xml (Listato 6.33). Di seguito vengono riportati i file jboss.xml e ejb-jar.xml. 6.9 Provare l’applicazione Per provare l’applicazione, innanzitutto bisogna compilare e avviare RMI. I passi da seguire per la compilazione sono i seguenti: Per compilare RMI: 1. Compilare i file ponendosi nella directory ...\RMIBookStore con l’istruzione javac Archivio\*.java 2. Compilare l’implementazione dell’interfaccia DataLayerImpl(in questo modo viene prodotto DataLayerImpl stub.class) attraverso l’istruzione rmic Archivio.DataLayerImpl per la creazione dello stub 3. Copiare lo stub nella directory del client 6.9 Provare l’applicazione <html> <%@ page contentType="text/html;Ãcharset=iso-8859-1" language="java" import="java.util.*,Ãjavax.naming.*, java.io.*, BookOrder.*, Archivio.*"ÃerrorPage=""Ã%> <head> <title>Gestione delle Ordinazioni</title> </head> <body bgcolor="#FFFFCC" text="#000000"> </style> </head> <% BookOrder carrello = (BookOrder)session.getAttribute("carrello"); if (carrello==null) { out.println("EffettuareÃilÃ<aÃhref=\"index.html\"> login</a> prima di continuare!"); return; } Book[] bookList = (Book[])carrello.getCarrello(); %> <b>Libri nel Carrello:</b> <table width="41%" border="0"> <tr> <td> <form method="get" action="DeleteBook"> <select size="3" name="DelLibro"> <% for(int i=0;i<bookList.length;i++) { out.print("<option>"); out.print(bookList[i].getTitolo()); out.println("</option>"); } %> Listato 6.22: Order.jsp 216 6.9 Provare l’applicazione </select></td> <td align="center" valign="middle">&nbsp;</td> </tr> <tr> <td width="45%" align="center" valign="middle"></td> <td width="55%" align="center" valign="middle"></td> </tr> <tr> <td align="center" valign="middle"> <input type="submit" value="Delete..."></td> </form> <form method="get" action="Pagamento.jsp"> <td align="center" valign="middle"> <input type="submit" value="ProcediÃalÃpagamento"></td> </form> Conto: <%= carrello.getConto() %> <td align="center" valign="middle"></td> </tr> </table> <p>&nbsp;</p> <p><b>Libri Disponibili:</b> </p> <table width="41%" border="0"> <tr> <td align="center"> <form method="get" action="Controller"> <select size="3" name="Biblioteca"> <% Book[] avail=carrello.getAvailableBooks(); for(int i=0;i<avail.length;i++) { out.print("<option>"); out.print(avail[i].getTitolo()); out.println("</option>"); } %> Listato 6.23: Order.jsp 217 6.9 Provare l’applicazione 218 </select></td> <td align="center" valign="middle">&nbsp;</td> </tr> <tr> <td width="45%" align="center" valign="middle"></td> <td width="55%" align="center" valign="middle"></td> </tr> <tr> <td align="center" valign="middle"> <input type="submit" value="AddÃto..."></td> </form> </tr> <tr> <td align="center" valign="middle"></td> <td align="center" valign="middle"></td> </tr> </table> </body> </html> Listato 6.24: Order.jsp 4. Mandare in esecuzione il registry con l’istruzione start rmiregistry 2000 (2000 è il numero di porta utilizzato nell’esempio considerato, è necessario scegliere il numero di porta perchè JBoss occupa quella utilizzata di default da RMI) 5. Registrare il Remote Server Object attraverso l’istruzione java Archivio.DataLayerReg Per la compilazione della servlet: BookStore.jar 1. BookStore.jar: compilo i file in Archivio, da BookStore.jar compilo i file in BookOrder: javac -classpath C:\<jboss home>\client\jboss-j2ee.jar;. BookOrder\*.java javac -classpath.;C:\<jboss home>\client\jboss-j2ee.jar BookOrderMDB\*.java 2. Accesso.war: compilo le classi java javac-classpath .; C:\<jboss home>\client\jbossj2ee.jar; C:\<jboss home>\client\javax.servlet.jar *.java Infine per il deploy: 1. Deploy delle servlet: Accesso.war in <jboss home> \. . . \ deploy 6.9 Provare l’applicazione <html> <%@ page contentType="text/html;Ãcharset=iso-8859-1" language="java", import="java.util.*, javax.naming.*, java.io.*, BookOrder.*, Archivio.*"ÃerrorPage=""Ã%> <head> <title>Pagamento</title> </head> <body> <% BookOrder b = (BookOrder)session.getAttribute("carrello"); %> <form method=GET action="PagamentoServlet"> Nome:<input TYPE=text SIZE=30 NAME="Utente"><br><p> Credit Card (16 cifre): <input TYPE=text SIZE=30 NAME="CC"><p> <tr> <td>Tipo:</td><p> <td> <input type="radio" name="tipo" value="0" checked>VISA<br> <input type="radio" name="tipo" value="1"checked>MASTERCARD<br> <input type="radio" name="tipo" value="2"checked>AMERICANEXPR<br><p> </td> </tr> <tr> <td colspan=2> <input type="submit" value="SendÃData"> </td> </tr> </form> </body> </html> Listato 6.25: Pagamento.jsp 219 6.9 Provare l’applicazione import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import javax.naming.*; import BookOrder.*; importjava.util.*; import javax.ejb.*; import java.net.*; public class Controller extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws IOException,ServletException { HttpSession s=req.getSession(); BookOrder b=(BookOrder)s.getAttribute("carrello"); String titolo=(String)req.getParameter("Biblioteca"); b.addBook(titolo); String messaggio="LibroÃaggiuntoÃalÃcarrello"; String link="Order.jsp"; OutputStream os=res.getOutputStream(); PrintWriter out=new PrintWriter(os); out.println("<HTML><HEAD><TITLE>OK</TITLE></HEAD>"); out.println("<BODY>"); out.println(messaggio); out.println("<aÃhref=\"" +link+ "\">Continua</a>"); out.println("</BODY></HTML>"); out.close(); } } Listato 6.26: Controller.java 220 6.9 Provare l’applicazione import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import javax.naming.*; import BookOrder.*; import java.util.*; import javax.ejb.*; import java.lang.*; public class PagamentoServlet extends HttpServlet { public void service (HttpServletRequest req, HttpServletResponse res) throws IOException,ServletException { //richiamo la sessione HttpSession s=req.getSession(); BookOrder b=(BookOrder)s.getAttribute("carrello"); String Uid=req.getParameter("Utente"); String CCString=req.getParameter("CC"); String tipoStringa=req.getParameter("tipo"); int tipoInt= Integer.parseInt(tipoStringa); String Carta=null; String messaggio=null; String messaggio2=null; String link=null; if (tipoInt==0) Carta="VISA"; if (tipoInt==1) Carta="MASTERCARD"; if (tipoInt==2) Carta="AMERICANEXPR"; Listato 6.27: PagamentoServlet.java 221 6.9 Provare l’applicazione if(b.closeOrder(CCString,tipoInt)) { messaggio="haiÃinseritoÃiÃseguentiÃdati:Ã"; messaggio2="pagamentoÃavvenutoÃconÃsuccesso!!!"; link="index.html"; //Annullo la sessione Servlet/JSP s.invalidate(); /*Notifico al Container che il Session Bean nonè necessario oltre */ try { b.remove(); } catch(RemoveException e) {e.printStackTrace();} } else { messaggio="iÃseguentiÃdatiÃsonoÃerrati:Ã"; messaggio2="verificaÃl’inserimento"; link="Pagamento.jsp"; } OutputStream os=res.getOutputStream(); PrintWriter out=new PrintWriter(os); out.println("<HTML><HEAD><TITLE> Pagina di conferma</TITLE></HEAD>"); out.println("<BODY>"); out.println("<b>"+messaggio+ "</b><br>"); out.println("<p>"); out.println("<b>Nome:Ã" +Uid+"</b><br>"); out.println("<p>"); out.println("<b>Carta:Ã"+Carta+ "</b><br><p>"); out.println("<b>"+messaggio2+"</b>"); out.println("<aÃhref=\"" +link+ "\">Continua</a>"); out.println("</BODY></HTML>"); out.close(); } } Listato 6.28: PagamentoServlet.java 222 6.9 Provare l’applicazione 223 import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import javax.naming.*; import BookOrder.*; import java.util.*; import javax.ejb.*; public class ConfirmServlet extends HttpServlet //fa da client del BookOrderBean { public void service (HttpServletRequest req, HttpServletResponse res) throws IOException,ServletException { String Uid=req.getParameter("Uid"); String password=req.getParameter("Password"); String messaggio = "<b>loginÃavvenuto con successo!</b>"; String link="Order.jsp"; OutputStream os=res.getOutputStream(); PrintWriter out=new PrintWriter(os); HttpSession sess=req.getSession(); //recupero l’oggetto session //porzione di codice per l’inizializzazione del contesto JNDI Properties prop = new Properties(); prop.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); prop.put(javax.naming.Context.URL_PKG_PREFIXES, "org.jnp.interfaces"); prop.put(javax.naming.Context.PROVIDER_URL, "localhost"); Listato 6.29: ConfirmServlet.java 6.9 Provare l’applicazione 224 try{ //inizializzazione del contesto javax.naming.Context ctx = new InitialContext(prop); //acquisizione dell’interfaccia HOME Object object = ctx.lookup("BookOrder"); BookOrderHome home = (BookOrderHome)javax.rmi.PortableRemoteObject .narrow(object, BookOrderHome.class); //utilizzo l’interfaccia home per creare l’EJB BookOrder bookServer = home.create(Uid,password); sess.setAttribute("carrello",bookServer); } catch (CreateException e){ //inviata da BookOrderBean messaggio="loginÃerrato!!!"; link="Index.html"; } catch (NamingException e) { messaggio="ImpossibileÃcreareÃcontesto:" + e.toString(); } out.println("<HTML><HEAD><TITLE> Login Page</TITLE></HEAD>"); out.println("<BODY>"); out.println(messaggio); out.println("<aÃhref=\"" +link+ "\">Continua</a>"); out.println("</BODY></HTML>"); out.close(); } } Listato 6.30: ConfirmServlet.java 6.9 Provare l’applicazione import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import javax.naming.*; import BookOrder.*; import java.util.*; import javax.ejb.*; import java.net.*; //import Archivio.*; public class DeleteBook extends HttpServlet { public void service (HttpServletRequest req, HttpServletResponse res) throws IOException,ServletException { HttpSession s=req.getSession(); BookOrder b=(BookOrder)s.getAttribute("carrello"); String titolo=(String)req.getParameter("DelLibro"); b.delBook(titolo); String messaggio="LibroÃeliminatoÃdalÃcarrello"; String link="Order.jsp"; /* ServletContext app = getServletContext(); RequestDispatcher disp; disp = app.getRequestDispatcher("/Order.jsp"); OutputStream os=res.getOutputStream(); PrintWriter out=new PrintWriter(os); out.println("<HTML><HEAD><TITLE> Libro eliminato</TITLE></HEAD>"); out.println("<BODY>"); out.println(messaggio); out.println("<a href=\"" +link+ "\">Continua</a>"); out.println("</BODY></HTML>"); out.close(); } } Listato 6.31: DeleteBook.java 225 6.9 Provare l’applicazione <?xml version="1.0" encoding="UTF-8"?> <web-appxmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <servlet> <servlet-name>ConfirmServlet</servlet-name> <servlet-class>ConfirmServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ConfirmServlet</servlet-name> <url-pattern>/ConfirmServlet</url-pattern> </servlet-mapping> <servlet> <servlet-name>Controller</servlet-name> <servlet-class>Controller</servlet-class> </servlet> <servlet-mapping> <servlet-name>Controller</servlet-name> <url-pattern>/Controller</url-pattern> </servlet-mapping> <servlet> <servlet-name>DeleteBook</servlet-name> <servlet-class>DeleteBook</servlet-class> </servlet> <servlet-mapping> <servlet-name>DeleteBook</servlet-name> <url-pattern>/DeleteBook</url-pattern> </servlet-mapping> <servlet> <servlet-name>PagamentoServlet</servlet-name> <servlet-class>PagamentoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>PagamentoServlet</servlet-name> <url-pattern>/PagamentoServlet</url-pattern> </servlet-mapping> </web-app> Listato 6.32: web.xml 226 6.9 Provare l’applicazione 227 <?xml version="1.0" encoding="UTF-8" ?> <jboss> <enterprise-beans> <session> <ejb-name>BookOrderEJB</ejb-name> <jndi-name>BookOrder</jndi-name> </session> <message-driven> <ejb-name>BookMDBean</ejb-name> <destination-jndi-name> topic/ordiniTopic </destination-jndi-name> </message-driven> </enterprise-beans> </jboss> Listato 6.33: jboss.xml 2. Deploy del topic ordiniTopic-service.xml in <jboss home>\. . . \deploy 3. Deploy dell’EJB: BookOrder.jar in <jboss home> \. . . \deploy Per la compilazione dell’interfaccia per il venditore: BookStoreSwing 1. Compilo le classi java javac -classpath C:\<jboss home>\client\jbossallclient.jar;. *.java 2. Per visualizzare l’interfaccia, mando in esecuzione la classe Application contenente il main java -classpath C:\<jboss home>\client\jbossall-client.jar;. Application 6.9 Provare l’applicazione <!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD Enterprise JavaBeans 2.0//EN’ ’http ://java.sun.com/dtd/ejb-jar_2_0.dtd’> <ejb-jar> <enterprise-beans> <session> <ejb-name>BookOrderEJB</ejb-name> <home>BookOrder.BookOrderHome</home> <remote>BookOrder.BookOrder</remote> <ejb-class>BookOrder.BookOrderBean</ejb-class> <session-type>Stateful</session-type> <transaction-type>Container</transaction-type> </session> <message-driven> <ejb-name>BookMDBean</ejb-name> <ejb-class>BookOrderMDB.BookMDBean</ejb-class> <message-driven-destination> <destination-type> javax.jms.Topic </destination-type> </message-driven-destination> <transaction-type>Container</transaction-type> </message-driven> </enterprise-beans> </ejb-jar> Listato 6.34: ejb-jar.xml 228 Capitolo 7 Web Service Secondo la definizione data dal W3C 1 un Web Service (servizio web) è un sistema software progettato per supportare l’interoperabilità tra diversi elaboratori su di una medesima rete. Il concetto di Web Service è molto simile a quello di oggetto, nel significato con cui lo si usa nella programmazione ad oggetti: un oggetto è un modulo software che offre una serie di funzioni utilizzabili dall’esterno da parte di un altro software tramite un interfaccia di comunicazione dichiarata dall’oggetto stesso. Allo stesso modo anche un Web Service offre una funzionalità, un servizio, ad altri client sulla rete attraverso un’interfaccia ben definita tramite il protocollo HTTP. Caratteristica fondamentale di un Web Service è quella di offrire un’interfaccia software basata su XML (descritta in un formato automaticamente elaborabile quale, ad esempio, il WSDL) tramite la quale comunica le funzioni che mette a disposizione e le relative modalità di utilizzo. L’XML può essere utilizzato correttamente tra piattaforme differenti (Linux, Windows, Mac) e differenti linguaggi di programmazione. XML è inoltre in grado di esprimere messaggi e funzioni anche molto complesse e garantisce che tutti i dati scambiati possano essere utilizzati ad entrambi i capi della connessione, essendo uno standard facilmente interpretabile grazie a software specifici; si può quindi dire che i Web Service sono basati su XML ed HTTP e che possono essere utilizzati su ogni piattaforma e con ogni tipo di software. Utilizzando tale interfaccia, altri sistemi possono interagire con il Web Service invocando le operazioni descritte nell’interfaccia tramite appositi messaggi inclusi in una busta SOAP: tali messaggi sono solitamente trasportati tramite il protocollo HTTP e formattati secondo lo standard XML. Questi messaggi sono utilizzati come un contenitore (envelope), dove le applicazioni inseriscono ogni informazione che deve essere spedita. Ogni envelope è formato da due par1 The World Wide Web Consortium 229 7.1 Le tecnologie alla base dei Web Services 230 Figura 7.1: Rappresentazione schematica di un messaggio SOAP ti: un’intestazione (header ) ed un corpo del messaggio (body) (Figura 7.1) L’header è opzionale, il body invece è obbligatorio, tutti i messaggi SOAP devono avere un corpo del messaggio. Sia l’header che il body possono avere sottostrati multipli nella forma di header blocks o body blocks. Il protocollo SOAP assume che ogni messaggio abbia un mittente ed un destinatario finale, e che un numero arbitrario di intermediari (detti nodi), processano il messaggio e lo instradano al destinatario. L’informazione che il mittente vuole trasmettere al destinatario sarà incapsulata nel body del messaggio, invece, ogni informazione aggiuntiva necessaria per il processamento intermedio è posta nell’header. L’idea base è quella di qualsiasi protocollo standard di comunicazione. La differenza sostanziale tra un servizio web offerto in Internet e un Web Service sta nel tipo di fornitore del servizio. Per i servizi web (come per esempio per gli accessi a banche o ad e-mail) è la persona fisica che accede direttamente attraverso una form html, mentre i Web Service sono a disposizione di programmi che, tramite il protocollo SOAP, si scambiano informazioni e quindi l’integrazione con il servizio avviene attraverso lo scambio di messaggi sulla rete. 7.1 Le tecnologie alla base dei Web Services Il protocollo di base per i Web Service è HTTP. Questo protocollo si occupa di mettere in comunicazione il Web Service con l’applicazione che intende usufruire delle sue funzioni. I Web Service si basano su tre tecnologie di 7.1 Le tecnologie alla base dei Web Services 231 fondamentali: SOAP (Simple Object Access Protocol) per la comunicazione. Le interazioni con i Web Service sono basate sul Simple Object Access Protocol (SOAP) per la definizione dei messaggi. Mediante l’uso di tale protocollo, è possibile lo scambio di messaggi usando uno standard che permetta la conversione del servizio in messaggi XML. SOAP sta al di sopra del livello applicativo/trasporto e un suo punto di forza è che questo protocollo può essere usato insieme a una varietà di protocolli (http, smtp, ftp, jabber e cosı̀ via). Il più usato è solitamente HTTP grazie alla sua vasta diffusione è adatto allo sviluppo di Web Service. Questo è dovuto alla sua semplicità, alla robustezza e alla possibilità di viaggiare sulla rete evitando problematiche legate ai firewall, come invece può accadere usando protocolli come CORBA o RMI. WSDL (Web Services Description Language) per la descrizione dell’interfaccia del servizio. Il Web Service Definition Language (WSDL) è il modo standardizzato in cui viene descritto il servizio e in cui vengono mostrate le sue interfacce. Questo protocollo è un XML-based IDL (Interface Definition Language) che introduce delle estensioni rese necessarie dalla mancanza di un middleware centralizzato. Attraverso il WSDL è possibile conoscere i metodi supportati dal Web Service e quali sono i loro input ed output. Grazie ad esso, è possibile generare lo stub e gli strati intermedi che rendono la chiamata al Web Service trasparente. In generale WSDL cerca di separare gli aspetti “astratti” della descrizione di un servizio da quelli “concreti” che lo legano a particolari protocolli di rete o di RPC. UDDI (Universal Description, Discovery and Integration) Per poter utilizzare i Web Service in modo dominante e su scala globale è necessario standardizzare i registri dei Web Service. Questo standard prende il nome di Universal Description Discovery and Indegration (UDDI), ovvero un linguaggio che rappresenta il repository che contiene il servizio. Tramite esso, gli utenti possono ricercare servizi adatti alle proprie necessità. Un UDDI registry è utilizzato con il significato di scoperta dei Web Service descritti usando WSDL. L’idea è che l’UDDI registry può essere cercato in vari modi per ottenere il contatto dell’informazione e la disponibilità dei Web Service per varie organizzazioni. L’UDDI potrebbe essere un modo di tenere aggiornato il Web Service che l’organizzazione ha appena utilizzato. Questi tre protocolli aperti sono alla base della interoperabilità tra i diversi sistemi software e tra le diverse piattaforme hardware. Grazie a questi proto- 7.1 Le tecnologie alla base dei Web Services 232 colli indipendenti dal linguaggio di programmazione o dal sistema operativo, è possibile far coesistere per esempio un client scritto in C con un server scritto in Java. Questo avviene grazie all’XML su cui, come già detto, sono basati i protocolli utilizzati. Nei prossimi paragrafi entreremo più nel dettaglio dei tre elementi caratterizzanti il Web Service e descritti sommariamente nel presente paragrafo. 7.1.1 SOAP: Simple Object Access Protocol Questo protocollo fornisce una via di comunicazione tra applicazioni eseguite su sistemi operativi diversi, con diverse tecnologie e linguaggi di programmazione, tramite HTTP ed XML. Acronimo di Simple Object Access Protocol, è uno standard nato in casa Microsoft e supportato in una seconda fase da IBM. Ora è uno standard W3C. Un messaggio SOAP è un documento XML che contiene i seguenti elementi: 1. Envelope, identifica il documento come un messaggio SOAP 2. Un elemento Header opzionale contenente informazioni specifiche per l’applicazione che permette di definire alcuni messaggi anche con diversi destinatari nel caso il messaggio dovesse attraversare più punti di arrivo 3. Body, elemento indispensabile che contiene le informazioni scambiate dalle richieste/risposte 4. Fault, elemento opzionale che fornisce informazioni riguardo ad eventuali errori manifestati durante la lettura del messaggio Le regole principali per realizzare un messaggio SOAP sono le seguenti: 1. Deve essere codificato con XML; 2. Non deve contenere il collegamento ad un DTD2 e non deve contenere istruzioni per processare XML Nelle specifiche SOAP convivono quattro diversi aspetti: 1. Il modello di scambio dei messaggi definisce un modo standard per definire come strutturare messaggi XML che saranno scambiati tra i diversi nodi, come gestire gli errori e come chiamare i tag XML che contengono informazioni. 2 Document Type Definition, definisce le componenti ammesse nella costruzione di un documento XML 7.1 Le tecnologie alla base dei Web Services 233 <?xml version="1.0"?> <SOAP-ENV:Envelope smlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/> <SOAP-ENV:Body> <ns1:getRate xmlns:ns1="urn:xmethods-CurrencyExcange"> <contry1>Inghilterra</contry1> <contry2>Giappone</contry2> </ns1:getRate> <SOAP-ENV:Body> <SOAP-ENV:Envelope> } Listato 7.1: Una richiesta SOAP 2. L’encoding SOAP fornisce una specifica (opzionale) per definire come strutture dati vengono rappresentate in XML. Essa è basata su una prima bozza delle specifiche XML Schema. 3. Il collegamento tra SOAP e HTTP. Il collegamento HTTP definisce il modo in cui i messaggi SOAP debbano essere veicolati e come devono essere utilizzate le intestazioni HTTP e i codici di ritorno per l’invio di messaggi SOAP. 4. L’utilizzo di SOAP come meccanismo di RPC. Il modello RPC definisce il modo in cui SOAP può essere utilizzato per rappresentare chiamate a procedure remote. Le specifiche SOAP consentono infatti di realizzare applicazioni distribuite di semplice messaggistica o più evolute. Anatomia del messaggio La comunicazione tramite SOAP avviene con lo scambio di messaggi strutturati in modo specifico comprendente elementi obbligatori ed elementi opzionali. Il tag principale che racchiude tutti gli altri è Envelope. Dentro questo sono presenti Header e Body. Il primo è opzionale, mentre nel corpo sono presenti tutte le informazioni applicative che il nodo vuole inviare. Nel listato 7.1 è presente un semplice messaggio SOAP. I tag Envelope e Body appartengono al namespace http://schemas.xmlsoap.org/soap/envelope. 7.1 Le tecnologie alla base dei Web Services 234 <?xml version="1.0"?> <SOAP-ENV:Envelope smlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/> <SOAP-ENV:Body> <ns1:getRateResponse xmlns:ns1= "urn:xmethods-CurrencyExcange"> <return>154.9423</return> </ns1:getRate> <SOAP-ENV:Body> <SOAP-ENV:Envelope> } Listato 7.2: La risposta L’utilizzo di SOAP-ENV è semplicemente una convenzione: al suo posto si sarebbe potuto utilizzare qualsiasi nome simbolico, come ad esempio ns1, utilizzato nel corpo del messaggio. Come si può notare, all’interno del tag Body è definito il messaggio applicativo: nel tag getRate sono presenti i tag country1 e country2. Questo blocco SOAP potrebbe essere un messaggio utilizzato in una applicazione che si occupa di fornire il tasso di conversione tra diverse valute. La risposta SOAP (Listato 7.2) è strutturata in modo da simulare una risposta applicativa. Per convenzione, il nome del tag utilizzato nella risposta è lo stesso della richiesta, a cui è aggiunta la parola Response. Dopo aver visto come è strutturato un messaggio SOAP, cerchiamo ora di capire dove dev’essere inviato il messaggio SOAP di richiesta per contattare il Web service e come si collega questo documento SOAP ad HTTP per raggiungere il Web service. Per rispondere bene a queste domande bisogna ricordare il tag <service> e gli attributi soapAction impostati all’interno del binding. Partiamo dal tag <service>. Al suo interno si sceglie il nome del nostro Web Service e si crea una porta tramite l’elemento <port> collegandola al binding SOAP. All’interno di quella porta si definisce un indirizzo scrivendo questa riga: <soap:address location=’’http://www.html.it/guida ai Web 7.1 Le tecnologie alla base dei Web Services 235 services/esempio.php/’’> L’attributo location indica ad un applicazione, che intende sfruttare il Web Service dove deve inviare le sue richieste HTTP. A questo punto SOAP viene incapsulato nel protocollo HTTP ed inviato sulla rete per raggiungere il Web Service. All’URL fornito naturalmente il Web Service è in attesa di ricevere delle richieste ed è pronto a dare le risposte più appropriate. Per far comprendere al Web Service quale azione deve intraprendere, l’applicazione aggiunge all’header HTTP una stringa in grado di indicare l’azione SOAP da intraprendere (cosı̀ come viene inserita nel binding per ogni operazione definita). 7.1.2 WSDL: Web Service Definition Language Un altro elemento fondamentale nell’insieme di tecnologie che supportano i Web Service è lo standard WSDL (Web Service Definition Language). Esso consente di descrivere un servizio in tutti i suoi aspetti. In un documento WSDL si trovano tutte le chiamate che si possono fare ad un Web Service, le specifiche delle strutture dati di input e output e le URL per accedere ai servizi. WSDL, a differenza di SOAP, non è uno standard legato a un livello di trasporto particolare ma è aperto all’utilizzo con protocolli differenti, specificando di volta in volta i bindings (collegamenti) verso il protocollo adottato. Concretamente, la descrizione di un Web Service è composta da: 1. I binding che legano ciascuna porta astratta a un protocollo e a un formato dati per i messaggi scambiati; 2. Dalle porte che legano le porte astratte istanziate dai binding ad indirizzi di rete reali; 3. I servizi composti da insiemi di porte correlate (che spesso offrono modalità differenti di accesso alla stessa tipologia di servizio). La definizione astratta dei Web Service di WSDL è composta da cinque diversi elementi: - I tipi (types) cioè strutture dati utilizzate come elementi di base per la costruzione dei messaggi di input e output; - I messaggi (messages) sono gli elementi che costituiscono gli input e gli output dei servizi; - Le operazioni (portType) sono le funzionalità offerte dal servizio; 7.1 Le tecnologie alla base dei Web Services 236 - I collegamenti (bindings) utilizzati per la mappatura del servizio astratto; - La definizione del servizio (service) necessaria per raccogliere tutte le operazioni sotto un unico nome. Lo standard WSDL privilegia il protocollo SOAP, indicando in una sezione delle sue specifiche le modalità di collegamento a questo standard. Avendo a disposizione un documento WSDL, un sistema software evoluto può dinamicamente invocare un Web Service ed elaborarne il risultato. Questo tipo di informazioni sono quelle che dovranno essere presenti all’interno del registry dei Web Service e costituiscono le specifiche tecniche per l’accesso a un servizio. L’utilizzo di WSDL ha diversi vantaggi tra cui la possibilità di avere a disposizione strumenti di sviluppo che sono in grado di prendere in input file WSDL e produrne codice che implementa l’infrastruttura per la realizzazione del servizio, inoltre è un modo per disaccoppiare il Web Service dal protocollo di trasporto e dai percorsi fisici. Come accennato, nel file WSDL è presente l’indicazione del protocollo da utilizzare (per esempio un sistema di runtime che supporti diversi protocolli di comunicazione XML potrebbe capire dal WSDL come ricondurre a messaggi SOAP reali le rappresentazioni astratte contenute nel documento WSDL). Un servizio potrebbe infatti partire utilizzando SOAP come protocollo per poi passare ad un nuovo tipo di protocollo con l’intento di ottimizzarne le prestazioni, ad esempio per via della sua potenziale minore occupazione di banda. All’interno di WSDL Un documento WSDL è un file XML contenente un insieme di definizioni. L’aspetto principale del WSDL è costituito dall’insieme di regole che consentono di definire in modo astratto un Web Service. Non meno importanti sono le specifiche per collegare il servizio a SOAP e HTTP. Tipi Per ogni messaggio scambiato nell’ambito delle operazioni WSDL deve essere definito il tipo. I tipi definiti da WSDL sono l’equivalente delle strutture (struct) del linguaggio C, cioè sono strutture dati anche complesse utilizzate come elementi di base per costruire i messaggi di input e output definiti nella sezione messaggi. Per definire i tipi usati all’interno del documento, WSDL usa l’elemento types. All’interno di types può essere inserito un intero 7.1 Le tecnologie alla base dei Web Services 237 <types> <schema targetNamespace="http://examples.com/stockquote.xsd" xmlns="http://www.w3c.org/2000/10/XMLSchema"> <element name="TradePrinceRequest"> <complexType> <all> <element name="ticjedSymbol" type="string"/> </all> </complexType> </element> <element name="TradePrince"> <complexType> <all> <element name="price" type="float"/> </all> </complexType> </element> </schema> </types> Listato 7.3: il primo elemento dei WSDL: i tipi schema XML o qualsiasi altra definizione di tipo valida e definibile tramite una notazione XML riconosciuta. A titolo di esempio, nel Listato 7.3 sono definiti due elementi TradePriceRequest, composto da una stringa, e TradePrice, composto da un valore in virgola mobile. In questo caso, dove la struttura dati contiene un solo campo, la definizione di un tipo specifico non è indispensabile: la sua utilità è per lo più relativa alla capacità di mantenere coerenza all’interno del documento WSDL. In questo modo infatti viene creato un alias e nel resto del documento WSDL si utilizzeranno i tipi definiti al posto dei tipi di dati primitivi, disaccoppiando il resto delle definizioni dai tipi di dati reali. Messaggi I messaggi sono la base della costruzione di un Web Service con WSDL. Sono gli elementi che costituiscono gli input e gli output dei servizi, e sono definiti in uno o più elementi message che seguono la sezione types. Ciascun messaggio ha un nome univoco ed è costituito da una o più parti, ciascuna 7.1 Le tecnologie alla base dei Web Services 238 <message name="getRateRequest"> <part name="contry1" type="xsd:string"/> <part name="contry2" type="xsd:string"/> </message> <message name="getRateResponse"> <part name="Result" type="xsd:float"/> </message> Listato 7.4: Un messaggio di esempio con un nome e un tipo distinti. I singoli messaggi possono contenere i tipi di dati complessi definiti nella sezione Type oppure semplici dati primitivi. La definizione dei messaggi segue immediatamente quella dei tipi, ciascun elemento message contiene una o più part, ognuna delle quali specifica una parte del messaggio con un determinato nome e tipo. Nel Listato 7.4 è mostrato un file WSDL del servizio mostrato nell’esempio precedente. Come si può notare osservando il listato, vengono definiti due diversi messaggi, uno relativo alla richiesta (getRateRequest) e uno relativo alla risposta (getRateResponse). Il primo definisce due elementi di tipo Stringa, chiamati Country1 e Country2, il secondo definisce un unico elemento di risposta Result di tipo float. Operazioni In tale sezione definiamo le operazioni fornite dal Web Service. Le operazioni equivalgono alle funzionalità che saranno esposte dall’interfaccia del servizio, sono definite in WSDL all’interno degli elementi operation; a loro volta gli elementi operation contengono elementi input, output e fault specificanti i messaggi scambiati durante l’operazione. Se si ponessero in relazione i Web Service con la programmazione distribuita (ad esempio RMI) le operazioni WSDL equivarrebbero ai singoli metodi di input e di output. Un’operazione di riferimento è presente nel Listato 7.5. L’elemento <operation>, come già detto, definisce una singola operazione e può contenere opzionalmente almeno uno dei due sottoelementi <output> ed <input>. La presenza e l’ordine di questi elementi determina la tipologia del servizio, che può rientrare all’interno di quattro diverse categorie: One-way. Anche detto fire&forget, è una configurazione dove l’endpoint si limita a ricevere il messaggio inviato dal client. In tal caso è presente 7.1 Le tecnologie alla base dei Web Services 239 <portType name="CurrencyExchangePortType"> <operation name="getRate"> <input message = "tns:getRateRequest" /> <output message = "tns:getRateResponse" /> </operation> </portType> Listato 7.5: Un’operazione di esempio un solo elemento di input, quindi il client spedisce un messaggio al servizio. Request-response. In tal caso l’endpoint riceve un messaggio di richiesta, esegue l’elaborazione necessaria e restituisce al client un messaggio di risposta correlato alla richiesta ricevuta. Sono presenti gli elementi input e output, quindi il client spedisce un messaggio al servizio e riceve una risposta. Solicit-response. E’ l’opposto del caso precedente. In questo caso infatti è l’endpoint che inizia la comunicazione inviando un messaggio al client che a sua volta dovrà rispondere, quindi il servizio invia un messaggio a un client e questo risponde. Nel file WSDL è presente prima l’elemento output e poi quello input. Notification. E’ l’opposto della tipologia one-way, l’endpoint invia un messaggio al client senza che questo debba inviare una risposta. E’ presente solo l’elemento output. Il modello di interazione da utilizzare dipende dalla natura del servizio. L’interazione request-response costituisce il tipico modello di comunicazione RPC quindi potrebbe essere quella usata per la maggior parte dei servizi, mentre le altre modalità potrebbero risultare più indicate in altri ambiti. Ad esempio una interazione one-way può essere utile in sistemi dove il client non è interessato al risultato dell’elaborazione dei dati inviati al servizio. La definizione di una operazione corrisponde alla dichiarazione di un metodo Java appartenente all’interfaccia di servizio. Il messaggio di input della operazione consta di argomenti e tipi corrispondenti, mentre il messaggio di output contiene solo il tipo. Qualora il messaggio di output fosse costituito da più parti, l’operazione definita non potrebbe avere un suo corrispondente Java, in quanto non potrebbe più essere interpretata come una RPC. 7.1 Le tecnologie alla base dei Web Services 240 <wsdl:binding name="nmtoken" type="qname">* <wsdl:operation name="nmtoken">* <wsdl:input>? </wsdl:input> <wsdl:output>? </wsdl:output> <wsdl:fault name="nmtoken"> * </wsdl:fault> </wsdl:operation> </wsdl:binding> Listato 7.6: Sezione WSDL relativa ai collegamenti Collegamenti In questa sezione avviene la mappatura del servizio astratto, definito nelle sezioni precedenti, con il protocollo concreto di comunicazione (ad esempio SOAP). Il contenuto di questa sezione è fortemente dipendente dal protocollo utilizzato e quindi dalle estensioni WSDL relative. Un binding è l’istanziazione di una porta astratta, ogni elemento bindings contiene un attributo type che si riferisce al particolare portType istanziato. La definizione del portType viene ripetuta aggiungendo nuovi elementi che specificano come codificare le operazioni e i messaggi. WSDL definisce gli elementi per esprimere vari tipi di binding: SOAP, HTTP, MIME. In WSDL, la struttura di un collegamento (semplificata) è mostrata nel Listato 7.6 Come si può notare, un collegamento può contenere diverse operazioni, ciascuna delle quali può avere un elemento di input, di output e una serie di elementi di errore (fault). Nel listato 7.7, in particolare, è presente un esempio di collegamento con SOAP. La struttura del collegamento è la seguente: sono definiti lo stile di codifica e il tipo di trasporto. Lo stile di codifica viene specificato dall’attributo style di soap:bindings e/o soap/operation, il quale può essere RPC oppure document. Se è di tipo RPC, allora il messaggio WSDL viene codificato come una RPC. All’interno del body del messaggio viene inserito un elemento con lo stesso nome dell’operazione e tanti figli quante sono le part del messaggio associato; se invece è di tipo document, il 7.1 Le tecnologie alla base dei Web Services 241 messaggio WSDL viene codificato usandone il contenuto letteralmente. In tal caso, ogni part del messaggio associato a un’operazione figurerà come figlio distinto dell’elemento Body nel messaggio SOAP. <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/> In questo caso la comunicazione è di tipo RPC e il trasporto avviene sul protocollo HTTP. In secondo luogo, è necessario definire come trattare i messaggi di input ed output per ciascuna operazione. Il contenuto degli elementi input e/o output che compongono ciascuna operation non deve essere ripetuto all’interno del binding; gli input e gli output conterranno invece dei nuovi elementi che specificano più in dettaglio come verrà costruito il corrispondente messaggio SOAP, in particolare quali informazioni dovranno essere mappate nell’Header e quali nel Body del messaggio. Per definire il contenuto del corpo del messaggio SOAP si usa l’elemento soap:body specificando: 1. Le parti del messaggio che saranno incluse nel corpo (attributo parts); 2. La codifica dei tipi interessati (attributi use e encodingStyle): (Use= encoded richiede la codifica di ogni parte in base al suo tipo secondo le regole di encodingStyle, Use=literal indica che le parti vanno copiate letteralmente nel messaggio). 3. Il namespace di provenienza degli elementi usati nel messaggio (attributo namespace) Il messaggio di input è cosı̀ definito: <soap:body use ="encoded" namespace="urn:xmethods-CurrencyExchange" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> In questo elemento viene indicato il namespace del messaggio (urn:xmethods - CurrencyExchange) e il tipo di encoding. L’attributo use assume valore encoding, quindi l’attributo encodingStyle dovrà indicare il tipo di encoding utilizzato. In questo caso viene utilizzato l’encoding SOAP. Gli elementi del namespace SOAP contenuti nell’elemento binding servono a descrivere come costruire un messaggio SOAP a partire dal messaggio astratto definito in WSDL. In questo caso viene utilizzato l’encoding SOAP. è mostrato un esempio di collegamento, relativo sempre al cambio valuta. 7.1 Le tecnologie alla base dei Web Services <binding name="CurrencyExchangeBinding" type="tns:CurrencyExchangePortType"> <soap:binding style="rpc"transport= "http://schemas.xmlsoap.org/soap/http"/> <operation name="getRate"> <soap:operation soapAction=""/> <input> <soap:body use ="encoded" namespace= "urn:xmethods-CurrencyExchange" encodingStyle= "http://schemas.xmlsoap.org/soap/encoding/"/> </input> <output> <soap:body use ="encoded" namespace= "urn:xmethods-CurrencyExchange" encodingStyle= "http://schemas.xmlsoap.org/soap/encoding/"/> </output> </operation> </binding> Listato 7.7: Esempio di un collegamento 242 7.1 Le tecnologie alla base dei Web Services 243 <service name="CurrencyExchangeService"> <documentation> Ritorna il tasso di scambio tra due valute </documentation> <port name="CurrencyExchangePort" binding="tns:currencyExchangeBinding"> <soap:address location= "http://service.xmethods.net:80/soap"/> </port> </service> Listato 7.8: Definizione del servizio Definizione del servizio L’ultimo elemento di un file WSDL è la definizione del servizio: questa sezione consente di raccogliere tutte le operazioni sotto un unico nome. Il servizio è identificato da un nome (l’attributo name dell’elemento service) e può avere una descrizione contenuta nel sottoelemento opzionale documentation. L’elemento service è l’elemento di livello più alto in WSDL. Esso dichiara un servizio web con un particolare nome come raccolta di porte. Al suo interno vengono elencate tutte le operazioni esposte dal servizio sotto forma di elementi port. Per ciascuno di questi viene indicato il collegamento utilizzato. Il contenuto dell’elemento port cambia in funzione del tipo di collegamento utilizzato. Nel Listato 7.8 è presente una definizione di servizio, in particolare del servizio di cambio valute, dove il collegamento utilizzato è quello con SOAP. In questo caso nell’elemento port è presente un elemento address appartenente al namespace soap (utilizzato in tutto il WSDL per riferirsi agli elementi strettamente relativi al collegamento di WSDL con SOAP). Questo elemento indica l’URL fisico dell’endpoint del servizio. L’utilizzo di WSDL può diventare anche molto complesso nel momento in cui ci si addentra in collegamenti e protocolli diversi da SOAP e HTTP. Per ciascun protocollo e livello di trasporto è infatti necessario definire una estensione di WSDL specifica e costruire i documenti WSDL secondo questa grammatica. Il vantaggio di grande estendibilità di WSDL si paga quindi in termini di complessità. 7.1 Le tecnologie alla base dei Web Services 7.1.3 244 UDDI: Universal Description Discovery and Integration L’UDDI (acronimo di Universal Description Discovery and Integration) è un registry (ovvero una base dati ordinata ed indicizzata) basato su XML ed indipendente dalla piattaforma hardware, che permette alle aziende la pubblicazione dei propri dati e dei servizi offerti su internet. UDDI, un’iniziativa open sviluppata tra il 1999 ed il 2000 e sponsorizzata dall’Organization for the Advancement of Structured Information Standards (OASIS: consorzio internazionale per lo sviluppo e l’adozione di standard nel campo dell’e-business e dei Web Services), permette quindi la scoperta e l’interrogazione dei servizi offerti sul web, delle aziende che li offrono e della maniera per usufruirne. Il registro UDDI è supportato da una rete mondiale di nodi collegati tra di loro in una sorta di federazione in modo similare alla tecnologia DNS. Quando un client sottopone una informazione al registro, questo la propaga agli altri nodi. In questo modo si attua la ridondanza dei dati fornendo una certa affidabilità. Il ruolo del singolo nodo rimane comunque fondamentale poichè nel momento in cui un client sottopone dei dati, questo ne diviene il proprietario, e sarà in futuro solo questo nodo a poter operare importanti operazioni sui dati quali la loro eliminazione. Le chiamate a UDDI sono implementate tramite comunicazioni SOAP di tipo request-response e permettono sostanzialmente due operazioni: quella di pubblicazione delle informazioni e quelle di ricerca delle stesse. Le informazioni gestite dal registro UDDI sono di tre tipi: - Pagine bianche. Contengono informazioni anagrafiche delle aziende, come l’indirizzo e i numeri di telefono. Costituiscono un primo approccio alle aziende, di tipo umano prima che tecnico. - Pagine gialle. Definiscono la categorizzazione (tassonomia) dei servizi e delle aziende. Un’azienda potrebbe essere infatti catalogata in base alla sua posizione geografica o al proprio settore industriale. Le tassonomie dovrebbero rispettare standard internazionali, quali quelli dettati dall’ISO. - Pagine Verdi. Costituiscono le informazioni tecniche dei servizi, quelle utili a livello informatico. Queste riguardano gli URL dei servizi o gli eventuali documenti WSDL. Nei registri UDDI, la descrizione dei Web Service contiene quattro tipi di informazioni: business information, service information, binding information, e informazioni sulle specifiche del servizio, descritte da diverse entità. 7.1 Le tecnologie alla base dei Web Services 245 businessEntity fornisce una descrizione delle organizzazioni che forniscono il Web Service. È una lista di nomi, indirizzi, telefoni e altri tipi di informazioni relative alla società fisica. businessService descrive un gruppo di Web Service offerti dalla società descritta nel businessEntity. Questo gruppo comprende servizi dello stesso tipo ma forniti in diversi indirizzi, diverse versioni o diverse tecnologie. Ad un businessEntity possono corrispondere diversi businessService, ma ad un businessService è associato uno ed un solo businessEntity. businessTemplate fornisce una descrizione tecnica necessaria per l’uso di un particolare Web Service. Essenzialmente esso definisce l’indirizzo dove reperire il servizio ed una serie di dettagli tecnici che descrivono l’interfaccia del Web Service o altre proprietà del servizio. Ad un businessService possono corrispondere diversi businessTemplate (uno per ogni interfaccia o indirizzo), ma ad un businessTemplate è associato uno ed un solo businessService. tModel acronimo di “technical model”, esso è il contenitore generico per ogni tipo di specifiche. Può rappresentare l’interfaccia di un servizio WSDL, una classificazione, un protocollo di interazione o può descrivere la semantica di un’operazione. Diversamente dalle entità precedenti, il tModel non è soggetto a nessuna relazione fissa. Riassumendo, se vogliamo pubblicare uno o più servizi in un registro UDDI, prima dobbiamo definire un’insieme di tModel che descrivono le diverse caratteristiche del servizio (come l’interfaccia WSDL o le operazioni svolte dal servizio). Successivamente bisogna pubblicare le informazioni sulla compagnia (businessEntity), le informazioni generali sul servizio proposto (businessService) e infine un set di informazioni tecniche per ogni diversa implementazione e punto di accesso (businessTemplate). JAXR L’accesso ai registri di servizi Web dalla piattaforma Java avviene tramite le Java API for XML Registries (JAXR). In generale si parla di registri perché JAXR ha un’architettura multiregistro, capace di accedere anche ad altre tipologie di registry, come ebXML R. Inoltre JAXR può propagare una singola ricerca di servizi a più registry contemporaneamente. L’accesso ai registri avviene tramite una connessione ottenuta dalla classe ConnectionFactory che viene opportunamente configurata tramite una serie di proprietà che 7.1 Le tecnologie alla base dei Web Services 246 indicano, tra le altre cose, l’URL del registro al quale si vuole accedere. Una volta in possesso della connessione, è possibile eseguire ricerche e realizzare la registrazione di aziende e servizi. Quest’ultima operazione è però sottoposta ad autenticazione e per poterla portare a termine è necessario essere registrati presso il particolare registro a cui si vuole accedere. Un aspetto interessante di JAXR è il suo uso del pattern futures (anche detto lazy-loading): una ricerca su un registro mondiale può potenzialmente restituire una grande mole di informazioni che non è garantito venga poi analizzata tutta. Per ovviare a questo inconveniente, ad ogni ricerca gli oggetti creati da JAXR non contengono tutte le informazioni ma solo quelle indispensabili. Nel momento in cui si renda necessario ottenere quelle mancanti, il runtime di JAXR esegue l’accesso ai dati necessari. Questa caratteristica ha due aspetti: per prima cosa, vengono recuperati solo i dati effettivamente necessari, con un guadagno in termini di performance; in secondo luogo, i client JAXR devono per forza essere sempre connessi alla rete in modo che i successivi caricamenti dati possano essere eseguiti correttamente. Le JAXR consentono la gestione delle informazioni presenti nel registro come la creazione, l’aggiornamento e l’eliminazione di elementi. Inoltre é supportata l’interrogazione del registro per eseguire le ricerche di servizi ed aziende. Ciascun elemento del registro é individuato univocamente da una UUID conforme al DCE 128 bit. Questa chiave é solitamente creata in modo trasparente dal registro, anche se per alcuni registri (come ebXML) é possibile fornire dall’esterno la chiave da utilizzare. Il ciclo di vita degli oggetti del registro inizia con l’operazione di creazione (l’interfaccia LifeCycleManager fornisce una serie di metodi di utilità per creare i differenti oggetti definiti nel modello informativo). Un oggetto creato non é ancora presente all’interno del registro e non può dunque essere oggetto di aggiornamenti o cancellazioni. Per inserire l’oggetto nel registro é necessario che questo passi attraverso un’operazione di salvataggio (save). Una caratteristica interessante di JAXR é la possibilità di deprecare gli oggetti, con un concetto simile al tag @deprecated di Javadoc. Gli oggetti deprecati non consentono nuove referenze (come associazioni o classificazioni), ma continuano a funzionare normalmente. Se da una parte l’interfaccia LifeCycleManager consente di gestire tutti gli elementi ad alto e basso livello del registro, dall’altra può essere necessario concentrarsi più su elementi di business. L’interfaccia BusinessLifeCycleManager contiene alcune importanti chiamate ad alto livello definendo una API simile alle Publisher API di UDDI. Con questa interfaccia non vengono introdotte nuove funzionalità ma vengono aiutati gli sviluppatori UDDI, che dovrebbero trovarsi ad operare con 7.1 Le tecnologie alla base dei Web Services 247 API più familiari. Un limite di JAXR in questa versione é che le operazioni sul ciclo di vita degli oggetti non sono applicabili a connessioni federate. Non é ad esempio possibile creare la stessa entità direttamente in due registri differenti. A differenza delle operazioni di disposizione (come la creazione o la modifica di informazioni), le operazioni di ricerca avvengono in modo non privilegiato. Non é necessaria dunque l’autenticazione dell’utente. L’interrogazione può avvenire in modo puntuale o tramite query. Nel primo caso viene utilizzata l’interfaccia BusinessQueryManager che, in modo similare a BusinessLifeCycleManager, ha una impostazione più ad alto livello e permette l’interrogazione delle interfacce più funzionali del modello informativo. Molti metodi dell’interfaccia richiedono argomenti simili, alcuni tra i più importanti sono: - findQualifiers. Definiscono le regole di confronto di stringhe, ordinamento e operatori logici sui parametri di ricerca. - namePatterns. E’ una Collection di stringhe che contengono nomi anche parziali con eventuali wildcard come specificato nelle SQL-92 per la parola chiave LIKE. - classifications. E’ una Collection di Classification che identifica in quali classificazioni eseguire la ricerca. Nel secondo caso é possibile utilizzare una query dichiarativa, tramite l’interfaccia DeclarativeQueryManager. Ad oggi l’unico standard supportato é una derivazione di SQL-92, in particolare dell’istruzione SELECT, estesa con le specifiche relative alle stored procedures. Le query dichiarative sono supportate anche su connessioni federate ma sono una caratteristica opzionale dei provider JAXR 1.0. Capitolo 8 Applicazione completa sui Web Service Questa semplice applicazione invia e riceve messaggi. 8.1 Il Web Service Per la realizzazione di un Web Service con JBoss, inizialmente si creano le classi Java: un’interfaccia che espone i metodi del web service (Listato 8.1) e la classe che implementa il Web Service (Listati 8.2 e 8.3). Successivamente, bisogna creare una serie di file XML che servono al Container per gestire il Web Service, per creare l’interfaccia WSDL e il file di mapping JAXR. Il file config.xml (Listato 8.4) definisce l’interfaccia del Web Service e serve per generare automaticamente il file WSDL e il file mapping.xml. package Archivio; import java.rmi.*; public interface Messaggio extends Remote { void sendMex(String mex) throws RemoteException; String receiveMex() throws RemoteException; } Listato 8.1: Messaggio.java 248 8.1 Il Web Service 249 package Archivio; import import import import java.rmi.*; java.util.*; java.rmi.server.*; java.io.*; public class MessaggioImpl extends UnicastRemoteObject { public MessaggioImpl() throws RemoteException { System.out.println("MessaggioImpl()"); } public void sendMex(String messaggio) throws RemoteException { Vector coda; System.out.println("send()"); try { coda = (Vector)new ObjectInputStream (new FileInputStream ("appoggio")).readObject(); } catch(IOException err) { coda=new Vector(); } catch(ClassNotFoundException clfe) { return; } coda.add(messaggio); try { new ObjectOutputStream (new FileOutputStream ("appoggio")). writeObject(coda); } Listato 8.2: MessaggioImpl.java 8.1 Il Web Service 250 catch(IOException err) { err.printStackTrace(); } } public String receiveMex() throws RemoteException { Vector coda; System.out.println("send()"); try { coda = (Vector) new ObjectInputStream (new FileInputStream ("appoggio")). readObject(); } catch(IOException err) { return(null); } catch(ClassNotFoundException clfe) { return(null); } try { String messaggio = (String) coda.get(0); coda.remove(0); new ObjectOutputStream (new FileOutputStream ("appoggio")). writeObject(coda); return(messaggio); } catch(NoSuchElementException nsee) { return(null); } catch(IOException err) { err.printStackTrace(); return(null); } } } Listato 8.3: MessaggioImpl.java 8.1 Il Web Service 251 <?xml version="1.0" encoding="UTF-8"?> <configuration xmlns= "http://java.sun.com/xml/ns/jax-rpc/ri/config"> <service name="ArchivioService" targetNamespace="http://archivio.ws.jboss.org/" typeNamespace="http://archivio.ws.jboss.org/types" packageName="Archivio"> <interface name ="Archivio.Messaggio"/> </service> </configuration> Listato 8.4: config.xml <?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>ArchivioWS</servlet-name> <servlet-class>Archivio.ArchivioImpl</servlet-class> </servlet> <servlet-mapping> <servlet-name>ArchivioWS</servlet-name> <url-pattern>/ArchivioUrl</url-pattern> </servlet-mapping> </web-app> Listato 8.5: web.xml La generazione di questi file avviene attraverso il tool wscompile che fa parte del Java Web Service Developer Pack (WSDP). Di seguito è mostrata la stringa di compilazione per questo Web Service: wscompile -classpath <classpath> -gen:server -f:rpcliteral -mapping config.xml Il classpath si riferisce alla directory dove trovare le classi java. A questo punto bisogna creare un package (nel nostro caso Archivio), all’interno del quale si pone una sottocartella con lo stesso nome del package in cui sono presenti i file .java. Il passo successivo consiste nella creazione del file web.xml (Listato 8.5) che non ha al suo interno elementi di configurazione ma serve unicamente per comunicare al Container come installare il Web Service. L’ultimo file xml da creare è webservices.xml (Listato 8.6). Quest’ultimo deployment descriptor è necessario a JBoss per comunicare che stiamo realizzando un Web Service e non una normale Servlet. Questo file comunica al Container dove trovare il file wsdl tramite l’elemento <wsdl-file>, mentre l’elemento <jaxrpc-mapping-file> indica 8.1 Il Web Service 252 <webservices> <webservice-description> <webservice-description-name> ArchivioService </webservice-description-name> <wsdl-file>WEB-INF/wsdl/ArchivioService.wsdl </wsdl-file> <jaxrpc-mapping-file> WEB-INF/mapping.xml </jaxrpc-mapping-file> <port-component> <port-component-name>Messaggio</port-component-name> <wsdl-port>MessaggioPort</wsdl-port> <service-endpoint-interface> Archivio.Messaggio </service-endpoint-interface> <service-impl-bean> <servlet-link>ArchivioWS</servlet-link> </service-impl-bean> </port-component> </webservice-description> </webservices> Listato 8.6: webservices.xml Figura 8.1: Schema WebService dove trovare il file mapping generato dal tool wscompile. Una volta creato anche quest’ultimo file, il Web Service è pronto per essere installato all’interno del container. Lo schema di come preparare i file è mostrato in Figura 8.1 Il deploy va eseguito come negli EJB, ponendo la cartella esempioWS.war in deploy all’interno di JBoss. Dopo pochi secondi nella finestra di avvio di 8.1 Il Web Service JBoss si può verificare l’avvenuta installazione. 253 8.2 Il Client 254 import Archivio.*; import javax.xml.rpc.Service; import javax.xml.rpc.ServiceFactory; import javax.xml.namespace.QName; import java.net.URL; public class send { public static void main(String[] args) throws Exception { String urlstr = "http://localhost:8080/esempioWS/ ArchivioUrl?wsdl"; System.out.println ("ContactingÃwebserviceÃatÃ" + urlstr); URL url = new URL(urlstr); QName qname = new QName ("http://archivio.ws.jboss.org/","ArchivioService"); ServiceFactory factory = ServiceFactory.newInstance(); Service service = factory.createService(url, qname); Messaggio arch = (Messaggio) service.getPort(Messaggio.class); arch.sendMex(args[0]); } } Listato 8.7: send.java 8.2 Il Client Di seguito viene illustrata la struttura del client, che compie due operazioni: invia e riceve messaggi. Sarà perciò composto da due classi di cui illustriamo il codice (Listati 8.7 e 8.8) e che servono a inviare un messaggio passato da linea di comando al Web service e a verificare l’avvenuta ricezione. Quando viene eseguito, il client come prima cosa cerca di connettersi al WSDL del Web Service all’indirizzo http://localhost:8080/esempioWS/ ArchivioUrl?wsdl. L’indirizzo di questo esempio si riferisce ad un Web Service presente nello stesso computer del client (localhost), nel caso sia diverso è necessario cambiare localhost nel nome o nell’indirizzo IP del server su cui il Web Service è dispiegato. La seconda parte dell’indirizzo esempioWS è il nome della cartella nella quale il Web Service è stato installato all’interno di JBoss. In questo esempio era esempioWS.war. L’ultima parte ArchivioUrl è il nome del pattern che si trova all’interno del file web.xml all’interno dei tag <url-pattern>. 8.2 Il Client 255 import Archivio.*; import javax.xml.rpc.Service; import javax.xml.rpc.ServiceFactory; import javax.xml.namespace.QName; import java.net.URL; public class receive { public static void main(String[] args) throws Exception { String urlstr = "http://localhost:8080/esempioWS/ArchivioUrl?wsdl"; System.out.println ("ContactingÃwebserviceÃatÃ" + urlstr); URL url = new URL(urlstr); QName qname = new QName ("http://archivio.ws.jboss.org/","ArchivioService"); ServiceFactory factory = ServiceFactory.newInstance(); Service service = factory.createService(url, qname); Messaggio arch = (Messaggio) service.getPort(Messaggio.class); String mex=arch.receiveMex(); System.out.println(mex); } } Listato 8.8: receive.java 8.2 Il Client 256 Figura 8.2: deploy del web service su JBoss Quest’indirizzo è visibile nella finestra di JBoss mostrata in Figura 8.2 dove vengono mostrate le quattro righe con cui JBoss comunica l’avvenuta installazione. Il passo successivo che il client esegue è la richiesta dell’interfaccia del web service, la classe Messaggio. Questo avviene creando un oggetto QNAME con i parametri del targetNamespace inserito nel file config.xml e il nome del servizio. Una volta ottenuta la classe di interfaccia del web service, si utilizzano i metodi esposti (sendMex() e receiveMex()) come se fossero in locale. Appendice Compilazione e Deploy1 di componenti J2EE in JBOSS . A.1 Servlet e JSP Vediamo quali sono i passi preliminari da compiere per poter provare e far girare una servlet. Per prima cosa, va installato JBoss. Per maggiori dettagli sull’installazione e il deploy si faccia riferimento [11] e [9]. Quando si vuol far girare una servlet con JBoss, bisogna predisporre con un certo criterio le classi java e gli altri file necessari (xml e jsp) che sono stati creati. Innanzi tutto, si crea una cartella nomecartella.war. All’interno di questa directory va creata una sottodirectory, che deve essere nominata WEB-INF. Al suo interno, la directory WEB-INF conterrà una sottodirectory classes con i file .java e .class della servlet; essa conterrà inoltre il file web.xml. Quando si crea un Progetto con JCreator e si vuole compilare, bisogna prima aggiungere il package javax.servlet.jar, nel modo seguente: dal menu Project si sceglie Project Properties e poi Required Libraries (vedi Figura 8.3), dopo di che si clicca il bottone Add e si seleziona Add Archive; a questo punto, verrà visualizzata una nuova finestra dalla quale sarà possibile accedere al package richiesto, dal seguente percorso: {Dir. installaz. di JBoss}\server\default\lib\javax.servlet.jar (vedi Figura 8.4). Una volta selezionato e aggiunto il package, gli si dà un nome simbolico, come mostrato in Figura 8.5. Dopo aver premuto il tasto 1 Con il termine Deploy si intende la messa in esercizio di un componente distribuito all’interno di un Application Server 257 Appendice 258 Figura 8.3: Finestra Set Library Figura 8.4: Finestra Apri OK, dalla finestra Set Library, sarà possibile selezionare il package rappresentato dal nome simbolico scelto (nel nostro caso Servlet), come mostrato in Figura 8.6. Infine, è possibile compilare la servlet, semplicemente attraverso il comando del menu Build\Compile Project. A.2 Uso di JMS con JBOSS I passi per la compilazione delle classi che fanno uso del package J2EE javax.jms sono analoghi a quelli per compilare le servlet. In questo caso, però, occorre importare l’archivio JAR: Appendice 259 Figura 8.5: Finestra Set Library Name Figura 8.6: Finestra Project Properties Finale Appendice 260 {Dir. installaz. di JBoss}\client\jbossall-client.jar Questa stessa libreria è anche richiesta durante l’esecuzione. Si osservi che, ovviamente, i produttori e i consumatori sia nel paradigmi Point-toPoint che Publish/Subscribe non devono essere in alcun modo dispiegati nell’Application Server. Configurazione di JNDI in JBOSS La creazione dello InitialContext in JBOSS e il successivo lookup delle Connection Factory e delle Destination richiede l’esistenza di un file il cui nome è jndi.properties che si deve trovare nel CLASSPATH. Tale file deve contenere le seguenti tre righe: java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.provider.url=jnp://localhost:1099 java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces In particolare la seconda riga contiene l’indirizzo IP o il nome simbolico del calcolatore su cui è installato JBOSS. É importante sottolineare che, sebbene la riga sia obbligatoria, di fatto può contenere quasi indirizzo IP o nome simbolico, anche se su computer indirizzato JBOSS non è installato (addirittura può non esistere il computer indirizzato!). Infatti, al momento di utilizzo, il client del protocollo JNDI prova ad accedere al computer con l’indirizzo specificato. Se tale computer non ha installato JBOSS oppure l’indirizzo non è valido, allora inizia una procedura di discovery per risolvere il contesto JNDI. Quindi, in pratica, a prescindere dal valore specificato nel contesto, la procedura di lookup va a buon fine se nella rete locale esiste un computer con JBOSS e su tale PC è effettivamente presente una Connection Factory o una destination con tale nome. Deploy delle destination L’unico oggetto che è necessario dispiegare all’interno di JBOSS per JMS sono ovviamente le Destination (Queue e Topic). Per dispiegare una specifica Destination occorre creare un file XML il cui nome deve essere del tipo XXX-service.xml che deve essere copiato in {Dir. installaz. di JBoss} \server\default\deploy\jbossall-client.jar Tale file deve contenere un unico elemento XML mbean come da esempio: <mbean code="org.jboss.mq.server.jmx.Topic" name="jboss.mq.destination:service=Topic,name=esempio"> Bibliografia 261 <depends optional-attribute-name="DestinationManager"> jboss.mq:service=DestinationManager</depends> </depends> </mbean> Tale esempio crea un Topic di nome esempio. Per creare una Queue è sufficiente sostituire nell’esempio tutte le volte in cui compare Topic con Queue. Bibliografia [1] J. Farley, W. Crowford, D. Flanagan, Java Enterprise in a Nutshell: A Desktop Qick Reference. [2] G. Alonzo, F. Casati, H. Kuno, V. Machiraju, Web Services: Concepts, Architectures and Applications, Springer-Verlag [3] Servlet Tutorial in Servlet API 2.0, http://javasoft-mirror.java.tin.it/products/servlet/index.html [4] The Java Tutorial, How to Use Root Panes, http://java.sun.com/docs/books/tutorial/uiswing/ components/rootpane.html [5] E. Gamma, R. Helm, R. Johnson, J. Vlissides, Design Patterns, Addison Wesley. [6] Java 2 Platform Standard Edition 5.0 API Specification http://java.sun.com/j2se/1.5.0/docs/api/ [7] J. Hunter, W. Crawford, Java Servlet Programming, O’Reilly. [8] H. Bergstein, Java Server Pages, O’Reilly. [9] S. Davis, T. Marrs, JBoss at Work: A Practical Guide, O’Reilly [10] J. Ball, D. Bode Carson, I. Evans, K. Haase, E. Jendrock, The Java EE 5 Tutorial For Sun Java System Application Server Platform Edition 9, http://java.sun.com/javaee/reference/ [11] The JBoss 4 Application Server Guide JBoss AS 4.0.3 Release 4, JBoss Inc. [12] J. Conallen, Communication Of The ACM October 1999, Vol. 42, No. 10 262