Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
La strada che porta alla realizzazione di ottime web application lastricata con il sudore e le angosce di coloro che ci hanno preceduti. L'idea di prendere una complessa applicazione client e farla funzionare su Internet stata certo coraggiosa, ma forse anche un po' avventata. Quando si parla di web applications non si intendono quelle piccole, semplici applicazioni che accettano input in uno o due moduli e poi trasferiscono tali informazioni a un database. Si intende parlare, invece, di quelle grandi applicazioni che attraversano svariati strati (tiers), utilizzando molte tecnologie differenti e interessando l'ambito dell'analisi e dell'utilizzo dei modelli architetturali (design patterns). La progettazione e la realizzazione di questo tipo di applicazioni spingono gli sviluppatori al limite di ci che logicamente e fisicamente possibile. Sono state proposte migliaia di soluzioni per i problemi posti da tali applicazioni: alcune soluzioni hanno preso campo e altre no, come ovvio quando non si risolvono le necessit degli utenti. Ma, come accaduto con l'evoluzione umana, le caratteristiche dimostratesi valide sono state trasmesse ai posteri, mentre quelle inadatte agli scopi prefissi sono scomparse con il procedere del tempo. L'interfaccia utente (User Interface, UI) presente in Smalltalk-80 era stata progettata sulla base dell'infrastruttura (framework) Model-View-Controller (MVC). Con il passare del tempo, questa infrastruttura stata adottata come pattern architetturale classico, ed stata utilizzata in molti scenari diversi. Altre infrastrutture UI, come quella di Java Swing, utilizzano concetti simili. Chi ha progettato l'architettura delle specifiche JavaServer Pages (TSP) ha introdotto in maniera formale il concetto astratto di due tipi di modello per le soluzioni basate sul web: il Model 1 e il Model 2. La caratteristica che li distingue che il Model 2 utilizza un componente separato per gestire le responsabilit del controller. Ci consente alle JSP di concentrare la loro azione esclusivamente sulla resa della vista utilizzando come modello i JavaBeans, divenendo cos in effetti una implementazione astratta di una architettura MVC web-based. L'infrastruttura Struts, creata da Craig R. McClanahan e donata alla Apache Software Foundation (ASF) nel 2000, un'implementazione open source dei concetti alla base del Model 2. Questo libro tratta della versione 1.1 di Struts ma, dal momento che la retrocompatibilit stato uno degli obiettivi nella progettazione della 1.1, anche chi utilizza la versione 1.0 di Struts pu trarre vantaggio dalla lettura del testo. Se c' un aspetto davvero importante da imparare con questo libro, che i framework come Strati rappresentano un ottimo investimento. Se si realizzano applicazioni, sia del tipo web-based che non, sar il caso di utilizzare almeno una infrastruttura di questo tipo e, nel campo dei framework web, Struts uno dei migliori
Questo capitolo tratta alcuni concetti preliminari, quali il modello MVC, il Model 2 e le caratteristiche di una infrastruttura software. Probabilmente molti sviluppatori saranno gi a conoscenza di alcune o di tutte le informazioni qui presentate, ma necessario garantire che tutti i lettori partano dalla stessa base di conoscenze: i concetti presentati in questo capitolo contribuiscono a creare le fondamenta per il resto del volume. Capitolo 2 All'interno dello strato web L'infrastruttura Struts framework si basa sulla tecnologia Java Servlet e, in misura minore, su quella delle JavaServer Pages, risultando perci strettamente correlata a un container web. Per gli sviluppatori Struts, la comprensione del modo in cui il web container elabora le richieste del client fondamentale ai fini della comprensione globale del framework stesso. Questo capitolo tratta i componenti del web container e le responsabilit di ciascuno di essi. Capitolo 3 Panoramica sul framework Struts Questo capitolo fornisce una panoramici dell'infrastruttura Struts, pur non trattandone tutte le caratteristiche o entrando in estremo dettaglio. Viene sottolineato il modo in cui tutte le parti si integrino nell'ambito dell'architettura MVC e Model 2 presentata nel Capitolo 1 Capitolo 4 Configurare le applicazioni Struts L'infrastruttura Struts utilizza due tipi separati di file di configurazione, i quali sono comunque interrelati. Tali file devono essere configurati in maniera corretta prima che un'applicazione possa funzionare nel modo corretto. Vista la diffusione e la flessibilit del linguaggio XML, entrambi i tipi di file di configurazione sono basati su di esso: il capitolo presenta la sintassi di questi file. Capitolo 5 I componenti controller di Struts L'infrastruttura Struts utilizza una servlet per elaborare le richieste in entrata; ci nonostante, essa pu fare affidamento su molti altri componenti che sono parte del dominio del controller per aiutarlo a portare a termine i suoi compiti. Questo capitolo tratta in maniera approfondita i componenti che gestiscono la funzionalit del controller nel framework. Capitolo 6 I componenti model di Struts Questo capitolo tratta i componenti che costituiscono la porzione model di una applicazione Struts. Il model rappresenta i dati business per un'applicazione e dovrebbe riprodurre in maniera molto fedele le entit reali e i processi logici dell'organizzazione. Questo capitolo esplora i ruoli e le responsabilit dei componenti del modello all'interno del framework Struts e si concentra sulla costruzione di una implementazione, corretta dal punto di vista dell'architettura, della applicazione Storefront. Particolare attenzione viene data all'utilizzo di un'infrastruttura di persistenza che possa essere integrata in un'applicazione Struts in maniera semplice e senza sforzo. Capitolo 7 I componenti view di Struts Il Capitolo 7 presenta i componenti che costituiscono la porzione view dell'infrastruttura Struts. Il framework utilizza i componenti della view per mostrare il contenuto dinamico al client. Basati principalmente sulle JavaServer Pages, tali componenti forniscono supporto per l'internazionalizzazione delle applicazioni al pari che per l'immissione dati da parte dell'utente, l'acccttazione e la validazione degli stessi e la gestione degli errori: tutto ci rende pi facile per lo sviluppatore concentrarsi sui requisiti dell'attivit. Il capitolo conclude la discussione sulle tre parti con cui il framework Struts implementa il pattern MVC. Capitolo 8 Librerie di tag JSP personalizzati Questo capitolo parla delle diverse categorie di tag e del modo in cui essi possono contribuire a rendere ancor pi semplice lo sviluppo di applicazioni con l'infrastruttura Struts. L'intento non di fornire un quadro di riferimento esaustivo su tutti i tag che fanno parte delle tag library di Struts tali informazioni sono reperibili all'interno del manuale utente di Struts o nei JavaDocs. Il vero scopo di questo capitolo di mettere in luce i benefici dell'utilizzo di tali librerie di tag di Struts e di fornire alcune strategie che possono rendere meno problematico il passaggio all'utilizzo dei tag. Capitolo 9 Estendere il framework Struts Uno dei maggiori vantaggi dell'utilizzo di un framework la possibilit di estenderlo e personalizzarlo sulla base delle necessit dell'applicazione. Il framework Struts non fa eccezione, e fornisce svariati e importanti punti di estensione per gli sviluppatori. Questo capitolo passa velocemente in rassegna alcuni di questi punti di estensione ed esamina i vantaggi e gli svantaggi dell'estensione dell'infrastruttura.
In questo capitolo si tratta del modo in cui usare il meccanismo di gestione delle eccezioni di Java nell'ambito delle proprie applicazioni Struts, per renderle pi robuste e metterle nelle condizioni di reagire in maniera elegante quando le cose non vanno come ci si aspettava. Viene prestata particolare attenzione alle differenze che sussistono tra la gestione delle eccezioni in maniera programmatica e quella effettuata in maniera dichiarativa tramite la nuova apposita funzionalit inserita nella versione 1.1 di Struts. Capitolo 11 Il framework Validator Questo capitolo presenta il framework Validator, appositamente creato per funzionare con i componenti di Struts. Il Validator consente di configurare in maniera dichiarativa le procedure di validazione; per un'applicazione Struts senza dover programmare una speciale logica di validazione Capitolo 12 Internazionalizzazione e Struts Il capitolo si concentra su quanto occorre fare per rendere una applicazione Struts disponibile a clienti di tutto il mondo, indipendentemente dalla lingua o dalla collocazione geografica. Come accade spesso nello sviluppo del software, una accurata pianificazione rappresenta uno dei passi pi importanti per contribuire al successo. Dopo la lettura del capitolo, il lettore dovrebbe essere in grado di realizzare applicazioni Struts che possano andare bene per una vasta gamma di destinatari. Capitolo 13 Struts ed Enterprise JavaBeans Il Capitolo 13 tratta gli argomenti che necessario prendere in considerazione quando si sviluppa un'interfaccia tra le azioni Stnits e lo strato di un'applicazione. Ci si concentra sull'interfacciamento con un modello costruito con gli Enterprise JavaBeans (EJB). Capitolo 14 Usare Tiles Questo capitolo tratta del framework Tiles, che fa ora parte della distribuzione di riferimento di Struts. L'infrastruttura (Tiles un framework di template (modelli standard di riferimento) che riduce la quantit di codice ridondante contenuta in una web application e consente agli sviluppatori una migliore separazione tra contenuto e presentazione. Capitolo 15 Il logging nelle applicazioni Struts Questo capitolo prende in esame il modo in cui l'utilizzo del logging, ovvero della registrazione delle attivit tramite opportuni file di log, possa aiutare a identificare i difetti prima che le applicazioni Struts vadano in produzione o, nel caso il software sia gi giunto a essere utilizzato in tale fase, il modo in cui il logging possa aiutare a identificare i problemi e ad arrivare alle soluzioni in maniera pi rapida. Capitolo 16 Packaging delle applicazioni Struts Questo capitolo tratta delle pratiche rivelatesi migliori per il confezionamento in package e per il dispiegamento (deploy) di una applicazione Struts, e di ci che necessario per automatizzare il processo di compilazione per il proprio ambiente. Particolare attenzione viene data ad Ant, il tool di compilazione basato su Java che disponibile in Jakarta. Capitolo 17 Attenzione alle prestazioni Il capitolo analizza le implicazioni sulle prestazioni derivanti dall'uso del framework Struts e delle tecnologie ad esso associate per realizzare web application, e tratta il modo in cui determinate decisioni sull'architettura e sulla programmazione influiscano sul complesso delle prestazioni delle applicazioni. Vengono trattate le verifiche sulle prestazioni, sul carico e sullo stress, con tutti i passi necessari per portarle a termine Appendice A Cambiamenti rispetto a Struts 1.0 L'appendice elenca le nuove caratteristiche della release 1.1. Appendice B Download e installazione di Struts L'appendice tratta le procedure da seguire per scaricare e installare Struts nel proprio ambiente operativo. Appendice C Risorse Questa appendice elenca svariate risorse che possono contribuire a un approfondimento delle conoscenze acquisite tramite questo libro.
Convenzioni grafiche
In questo libro vengono usate le seguenti convenzioni grafiche: il carattere cosivo utilizzato per: percorsi Unix, nomi di file e programmi indirizzi Internet, nomi di dominio e URL termini nuovi la prima volta che vengono definiti
Il carattere grassetto utilizzato per:
linee di comando e opzioni che dovrebbero essere digitate cos come sono presentate nomi e parole chiave di programmi Java, compresi nomi di metodi, di variabili e di classi. nomi di elementi e tag XML, di attributi, e altri costrutti XML che appaiono cos come sono realmente in un documento XML L'immagine a fianco, associata al carattere piccolo, denota un'indica un consiglio o una zione, nota in genere.
Commenti e domande
possibile indirizzare all'editore commenti e domande su questo libro:
O'Reilly & Associates, Inc. 1005 Gravenstein Highway North Sebastopol, CA 95472 800-998-9938 (in the U.S. or Canada) 707-829-0515 (international or local) 707-829-0104 (fax)
Esiste una pagina web relativa al presente libro, che contiene glierrata corrige [nella traduzione italiana, gli errori segnalati sono gi stati corretti, N.d.T.],gli esempi e ulteriori informazioni. L'indirizzo di tale pagina :
http://www.oreilly.com/catalog/jakarta/
L'e-mail a cui inviare commenti o fare richieste di tipo tecnico concernenti il libro :
bookquestions@oreilly.com http://www.oreilly.com
Per ulteriori informazioni su libri, conferenze, Resoti ree Centcrs e la O'Reilly Network, si pu visitare il sito web O'Reilly:
Ringraziamenti
Scrivere un libro come questo non comporta mai solamente il lavoro di un'unica persona; ci vuole davvero un esercito di soldati temprati e questo libro non fa eccezione. A partire dagli editor, per passare all'organizzazione commerciale, fino a chi ha corretto il manoscritto, semplicemente questo libro non sarebbe stato possibile senza il gruppo appassionato di persone che hanno rinunciato spesso al riposo e ai fine settimana per garantire che la qualit fosse la pi elevata possibile. Tutti i meriti vanno alle eccezionali persone che hanno preso parte a tale realizzazione, mentre mi assumo la responsabilit di ogni errore. Per primi, devo ringraziare i miei due editor alla O'Reilly, Brctt McLaughlin e Robert Eckstein, tra i redattori pi appassionati e sinceri con cui abbia mai lavorato. Mi auguro che ci sia la possibilit di lavorare ancora irfsieme su progetti futuri. I vostri consigli e la vostra guida hanno reso il compito molto pi facile di quanto avessi potuto immaginare. Vorrei inoltre ringraziare Kyle Hart della divisione commerciale per il suo aiuto, davvero molto apprezzato. Vorrei poi ringraziare la squadra di ottimi sviluppatori con i quali ho inizialmente imparato Struts. Il legame della nostra piccola squadra NV stato qualcosa di speciale che si verifica solo di quando in quando: ho davvero apprezzato il lavoro insieme a tutti voi. Le seguenti persone hanno svolto un ruolo speciale, fornendomi consigli sul contenuto del libro e sull'indirizzo da tenere: Steve Ardis, Jill Entinger, See Yam Lim e Ted Husted (anche se Ted probabilmente non si render conto del suo contributo in tal senso). Quando ho deciso di rendere disponibili le bozze dei capitoli del libro, dissi che, se chi li leggeva avesse avuto la gentilezza di fornire dei riscontri, avrei ringraziato i "revisori" in questa sezione del libro... be' non mi ero reso conto di quanti mi avrebbero preso in parola! Si tratta di cos tante persone che non posso ringraziarle tutte singolarmente. Pi di cento lettori hanno fornito feedback per uno o pi capitoli del libro. La comunit Struts di certo una delle migliori: quasi tutti gli iscritti alla mailing list di Struts hanno fornito il loro contributo, in un modo o nell'altro. A ciascuno di loro va il mio grazie. Grazie a: John Guthrie, David Karr, Brent Kyle, Stefano Scheda e Rick Reumann per aver posto le giuste domande, a Mark Galbreath per avermi fatto ricordare della mia insegnante di Inglese delle scuole medie e a James Mitchcll e James Holmes per avermi coinvolto nel gruppo Struts di Atlanta.
Devo un ringraziamento speciale a Tim Drury per il suo aiuto sul Capitolo 16. Le sue competenze su Ant sono riconosciute a livello mondiale. Ringraziamenti speciali anche a Brian Keeton, che ha scritto il Capitolo 13 perch io sono troppo lento: il miglior sviluppatore di EJB che conosca. Si tratta in entrambi i casi di ottimi amici, semplicemente tra gli esseri umani migliori che io abbia mai incontrato: il solo fatto di conoscerli mi ha reso una persona migliore. Mi auguro sia possibile lavorare ancora insieme. Il gruppo di TheServerSide.com merita uno speciale grazie da parte mia e dei revisori per aver reso disponibili in download le bozze dei capitoli di questo libro: un esempio notevole di ci che il futuro riserva alle pubblicazioni librarie. Sarebbe una grave negligenza non ringraziare l'intera comunit che gravita intorno a Struts: ho intrattenuto corrispondenza con molti intelligenti sviluppatori che si sono oltretutto rivelati persone divertentissime con cui trascorrere il tempo, nei casi in cui poi ci siamo conosciuti di persona. Il feedback e i consigli ricevuti durante questo processo sono stati formidabili: spero che tutti ne abbiano tratto un qualche beneficio e apprezzo la pazienza di tutti. Infine, tutti gli sviluppatori Struts dovrebbero ringraziare Craig McClanahan che ha avuto l'intuizione e le capacit per creare questo framework e renderlo disponibile come software libero per la comunit. Avrebbe potuto pensare di "farci i soldi", mettendo su una "Struts, Inc.", ma si reso conto che, seguendo l'altra strada, sarebbe comunque stato ricompensato moltissimo, e credo che cos sia successo. Craig, un ottimo framework, di cui devi andare orgoglioso. E infine, a tutti coloro che si sono impegnati nel progetto Struts: che cosa sarebbe stato veramente, senza di voi? Grazie per tutto il vostro aiuto.
CAPITOLO 1
Introduzione
Il framework open source Struts stato creato per facilitare chi sviluppa web application basate sulle tecnologie Servlet Java e JavaServer Pages (JSP). Come nel caso di un edificio, una web application deve avere delle solide fondamenta a partire dalle quali pu essere eretta la struttura restante. Il framework Struts fornisce agli sviluppatori una infrastruttura unificata sulla base della quale possono essere costruite applicazioni Internet. Usare Struts come base di partenza consente agli sviluppatori di concentrarsi sulla costruzione delle applicazioni rispondenti alle esigenze reali invece di focalizzarsi sull'infrastruttura stessa. L'infrastruttura Struts stata realizzata da Craig R. McClanahan ed stata donata alla Apache Software Foundation (ASF) nel 2000. Il progetto coinvolge adesso in maniera cospicua diversi soggetti in tutto il mondo, e molti sono gli sviluppatori che contribuiscono alla buona riuscita del framework. Struts uno dei molti progetti ben noti e coronati da successo che fanno parte del grande quadro Apache Jakarta, tra i quali si possono ricordare Ant, log4j e Tomcat. L'obiettivo finale del progetto Jakarta di fornire, in maniera aperta e cooperativa, soluzioni server basate sulla piattaforma Java che possanoreggere gli standard di mercato.
Microsoft. Sebbene queste soluzioni forniscano spesso migliori prestazioni e maggiore scalabilit rispetto ai programmi CGI standard, hanno comunque i loro problemi di portabilit. Nel 1997, quando il linguaggio Java stava attraversando un periodo di straordinaria crescita e diffusione tra gli sviluppatori di applicazioni, fu creata la tecnologia Java Servlet. Questa nuova tecnologia per il web ha aperto una nuova strada pronta per essere esplorata dagli sviluppatori web.
Per le proprie servlet, gli sviluppatori sono liberi di scegliere tra i molti container disponibili: non dipendono da un particolare produttore o da una specifica piattaforma. Le servlet sono portabili e possono migrare attraverso diversi container, senza il bisogno di ricompilare il codice sorgente o di effettuare delle modifiche. Tutto ci porta a una situazione in cui si salvano "capra e cavolo", ideale per le web application: il miglior prodotto o componente per una particolare esigenza, evitando allo stesso tempo gli alti rischi legati normalmente alla scelta di una singola soluzione. Sul mercato sono disponibili diversi servlet container che hanno raggiunto ampia diffusione. Alcuni di essi sono container standalone che, per funzionare, devono essere connessi a un web server esterno, mentre altri forniscono sia il web server che il servlet container nello stesso prodotto. Ce ne sono anche alcuni che sono integrati in application server e forniscono tante funzionalit in pi rispetto a un semplice servlet container. Alcuni sono prodotti commerciali, mentre altri hanno un costo piccolo o insignificante.
Servlet container Bluestone Borland Enterprise Server iPlanet Application Server Orbix E2A (formally iPAS) Jetty JRun Orion Application Server Resin SilverStream Apache Tomcat Weblogic Application Server WebSphere EAServer http://www.bluestone.com http://www.inprise.com
URL
http://www.sun.com/software/iplanet/ http://www.iona.com http://www.mortbay.com http://www.macromedia.com/software/jrun/ http://www.orionserver.com http://www.caucho.com http://www.silverstream.com http://jakarta.apache.org/tomcat/ http://www.bea.com http://www-4.ibm.com/software/webservers/appserv/ http://www.sybase.com
Per un elenco pi completo di servlet container disponibili, si pu visitare il sito della Sun dedicato all'industria delle servlet, all'indirizzo http://java.sun.com/products/servlet/industry.html. Sebbene le servlet siano ottime nello svolgere i loro compiti, presto risultato evidente che incorporare stabilmente output HyperText Markup Language (HTML) all'interno di una servlet come risposta a una richiesta presentava alcuni seri limiti. Innanzitutto, risultava difficile effettuare cambiamenti all'HTML, poich per ogni cambiamento era necessario ricompilare la servlet. In secondo luogo, il supporto per lingue diverse era difficile poich l'HTML inglobato stabilmente (hardcoding). Determinare lingua, regione e altre varianti relative all'utente e poi mostrare l'output qualcosa che si riesce a fare con difficolt. Molte applicazioni web costruite con le servlet tralasciano volutamente tutti i problemi di internazionalizzazione [1] rendendo disponibili servlet differenti, una per ogni locale supportato.
[1]
Ci si riferisce comunemente all'internazionalizzazione con la sigla "I18N" derivante dal fatto che la parola inglese internationalization comincia con la lettera I, finisce con la lettera N, e tra queste due sono contenuti 18 caratteri.
Infine, dal momento che l'HTML era inglobato all'interno della servlet, c'era un problema con le responsabilit. I web designer costruiscono pagine HTML ma la maggior parte di loro non ha, in genere, grande esperienza di linguaggio Java, per non parlare di profonda conoscenza delle architetture e della programmazione objectoriented. Quando si mescolano codice HTML e Java dentro una servlet, diventa difficile separare il design della pagina e i compiti di programmazione. Persino quando uno sviluppatore ha le abilit necessarie per svolgere entrambe le funzioni, qualunque modifica all'impianto grafico della pagina richiede una ricompilazione, che va ad aumentare i tempi di sviluppo e di verifica. La programmazione delle servlet un argomento cos vasto che non pu essere trattato qui con grande dettaglio. Maggiori informazioni sulla tecnologia delle Java Servlet si possono trovare nell'ottimo Java Servlet Programming di Jason Hunter [ed. originale O'Reilly, pubblicato in Italia da Hops Libri, N.d.T.]. anche possibile visitare il sito della Sun dedicato a tale tematica (http://java.sun.com/products/servlet/index.html). Le JavaServer Pages hanno rappresentato il passo successivo nella progressione di sviluppo delle tecnologie web basate sulla piattaforma Java. L'introduzione delle pagine JSP ad esse ci si riferisce normalmente cos ha contribuito a superare i limiti delle serviet appena ricordati, aprendo molte nuove porte per gli sviluppatori web.
Il "traduttore" che trasforma il file jsp in uno java si fa carico del noioso lavoro di creare una serviet Java a partire dalla pagina JSP. La Figura 1-2 illustra il modo in cui una pagina JSP viene tradotta e compilata in una servlet.
Figura l-2. Una pagina JSP viene tradotta e compilata in una servlet Java
La tecnologia delle JSP diventata una soluzione estremamente diffusa per costruire web application utilizzando la piattaforma Java. JSP presenta diversi vantaggi rispetto alle tecnologie concorrenti: JSP una specifica, non un prodotto. Gli sviluppatori possono scegliere la loro implementazione ideale. Le pagine JSP sono compilate, non interpretate, con un miglioramento delle prestazioni. Le pagine JSP supportano sia gli script che un accesso completo al linguaggio Java e possono essere estese tramite custom tag. Le pagine JSP condividono la caratteristica Write Once, Run Anywhere della tecnologia Java. Come accennato nella sezione precedente, uno dei limiti dell'hardcoding di HTML dentro le servlet quello di separare le responsabilit del design della pagina dalla programmazione della logica dell'applicazione. Tale separazione risulta pi facile con le pagine JSP perch i grafici HTML sono liberi di creare pagine web con lo strumento che preferiscono (gran parte dei tool pi diffusi oggigiorno sono in grado di lavorare con JSP e custom tag). Quando il layout della pagina stato stabilito dal web designer, gli sviluppatori JSP possono inserire scriptlet JSP e custom tag e salvare il file con estensione .jsp. Tutto qui. Quando arriva il momento di modificare o la grafica, oppure la logica della pagina, lo sviluppatore modifica la pagina JSP come necessario ed essa viene ricompilata automaticamente. Nel loro insieme, pagine JSP e servlet sono un'attraente alternativa rispetto ad altri tipi di programmazione dinamica per il web. Poich sono entrambe basate sul linguaggio Java, garantiscono indipendenza dalla piattaforma, estensibilit nell'ambito enterprise e, cosa ancora pi importante, semplicit di sviluppo.
Va notato che nel processo non viene coinvolta alcuna servlet extra. La richiesta del client viene inviata direttamente a una pagina JSP, la quale potrebbe comunicare con dei JavaBeans o altri servizi, ma in definitiva la JSP che seleziona la pagina successiva per il client. La vista successiva viene determinata sulla base della JSP selezionata oppure di parametri contenuti nella richiesta del client. Diversamente, nell'architettura Model 2 la richiesta del client viene intercettata prima da una servlet, detta controller servlet. Questa gestisce l'elaborazione iniziale della richiesta e determina quale pagina JSP deve essere visualizzata come successiva. Questo approccio viene illustrato in Figura 1-4.
Come si vede, nell'architettura Model 2 un client non invia mai direttamente una richiesta a un pagina JSP. Ci permette alla servlet di effettuare una elaborazione in front-end comprendente autenticazione e autorizzazione, logging centralizzato, e internazionalizzazione. Una volta che stata completata l'elaborazione della richiesta, la servlet dirige la richiesta alla giusta pagina JSP. Il modo in cui viene determinata la pagina successiva da mostrare varia a seconda delle applicazioni. Per esempio, in applicazioni pi semplici, la pagina JSP successiva da mostrare potrebbe essere inglobata nella servlet, basandosi su richiesta, parametri e stato corrente dell'applicazione. In web application pi sofisticate potrebbe essere utilizzato un motore di flusso/regole. Come si evince, la differenza principale fra le due architetture sta nel fatto che il Model 2 inserisce una servlet controller, la quale fornisce un punto di accesso singolo e incoraggia un riutilizzo e una estensibilit maggiore rispetto all'approccio del Model 1. Con l'architettura Model 2, c' chiara separazione tra business logic, visualizzazione presentata ed elaborazione della richiesta. Si fa spesso riferimento a questa separazione come a un pattern Model-View-Controller (MVC). Sebbene l'architettura Model 2 possa sembrare eccessivamente
complicata, in pratica essa pu semplificare enormemente un'applicazione. Le web application costruite usando l'architettura del Model 2 hanno in genere una manutenzione pi facile e possono essere estese maggiormente rispetto ad applicazioni confrontabili che per sono costruite sull'architettura Model 1.
Il "modello" si occupa della conoscenza del dominio dell'applicazione La "vista" si occupa della visualizzazione del dominio dell'applicazione Il "meccanismo di controllo" si occupa di verificare il flusso e Io stato dei dati inseriti dall'utente
Con il pattern MVC, si verifica di solito una notifica degli eventi per comunicare alla View che qualche parte del Model cambiata. Tuttavia, dal momento che nella tipica web application un browser mantiene una connessione stateless, la notifica dal modello alla vista non si verifica in maniera semplice. [2] Ovviamente, un'applicazione potrebbe effettuare un qualche tipo di meccanismo di push (= "spingere") per inviare notifiche o dati a un client, ma ci eccessivo per la maggior parte delle web application. Un utente pu chiudere il browser in ogni momento e, di solito, nessuna notifica viene inviata al server. Occorre un impegno eccessivo per gestire client remoti dal lato server. Questo tipo di comportamento non necessario per le tipiche web application B2C e B2B.
[2] Le web application sono considerale stateless (= "prive di stato") poich il browser non mantiene costantemente una connessione aperta con il server web. Ci nonostante, una web application pu comunque mantenere dati di sessione per un utente o addirittura salvare i dati all'interno del browser su richiesta dell'utente.
Con normali web application, un client di solito invia un'altra richiesta al server per sapere se ci sono stati cambiamenti al Model. Questo viene generalmente definito come approccio di tipo pull (= "tirare"). Per esempio, se un utente sta osservando le informazioni relative al prezzo di un articolo e proprio in quell'istante l'amministratore cambia proprio quel prezzo, l'utente non sapr che il costo dell'articolo cambiato fino a che non effettuer un nuovo caricamento della pagina.
In una applicazione enterprise pi complessa (dove il tier web comunica con un server EJB, per esempio) la porzione Model del pattern MVC sar rappresentata da Enterprise JavaBeans. Sebbene le specifiche EJB 2.0 abbiano segnato un miglioramento delle prestazioni attraverso l'utilizzo di interfacce local, si pu ancora verificare un impatto significativo sulle prestazioni se il tier web tenta di utilizzare gli entity bean direttamente, come porzione Model dell'applicazione, a causa del peso di dover effettuare chiamate remote. In molti casi, JavaBeans vengono restituiti da session bean e usati all'interno del tier web. Questi JavaBeans, di solito detti data transfer object o value object, vengono utilizzati all'interno delle View per costruire il contenuto dinamico.
4. 5.
Intercetta le richieste HTTP provenienti da un client Traduce ciascuna richiesta in una specifica operazione appartenente alla logica business da effettuare Invoca tale operazione o la delega a un handler (meccanismo di riferimento) Contribuisce alla scelta della successiva vista che deve essere mostrata al client Restituisce la View al client
Il pattern Front Controller, che fa parte dei design pattern della piattaforma Java 2 Enterprise Edition (J2EE) (ulteriori informazioni presso http://java.sun.com/blueprints/patterns/index.html), descrive il modo in cui dovrebbe essere implementato il controller di un tier web. Dal momento che tutte le richieste del client e le risposte ad esso passano attraverso il controller, esiste un punto di controllo centralizzato per la web application. Ci ha i suoi vantaggi quando si tratta di aggiungere nuove funzionalit. Codice che andrebbe normalmente messo in ogni pagina JSP pu invece essere sistemato nella servlet controller, la quale elabora tutte le richieste. Il controller contribuisce inoltre alla separazione dei componenti di presentazione (viste) dalle operazioni business dell'applicazione, con un ulteriore facilitazione dello sviluppo.
componenti generici che collaborano tra loro, i quali possono essere estesi dalla nostra applicazione per fornire una serie particolare di funzioni. I "luoghi" in cui il framework pu essere esteso sono detti punti di estensione. Ci si riferisce spesso ai framework come a "librerie alla rovescia" per il modo alternativo in cui essi operano. La Figura 1-5 illustra le sottili differenze tra framework e library.
Un committer quello sviluppatore esperto che, assumendo un ruolo di "timoniere", fornisce indicazioni e consigli per mantenere sulla giusta rotta lo sviluppo dell'infrastruttura Struts. Un committer ha la possibilit di modificare il repository del codice sorgente e il diritto di voto nelle questioni che influiscono sul futuro di Struts.
Certo, pu sembrare strano presentare la possibilit di costruirsi il proprio framework come alternativa all'uso di Struts. Perch mai costruirsi framework a partire da zero, quando gi ne esistono di tipi diversi? La ragione la stessa per cui vengono sviluppati altri prodotti open source o commerciali. La selezione disponibile di prodotti potrebbe semplicemente non rispondere in maniera sufficientemente adeguata al framework di cui si ha veramente bisogno, e potrebbe quindi essere preferibile realizzarne uno "in casa". Il miglior consiglio che posso fornire sulla "autocostruzione" di un framework di porsi una serie di domande: 1. 2. 3.
4.
Ho impiegato tutto il tempo necessario per rendermi conto di ci che gi esiste e per costruire un prototipo utilizzando un framework gi esistente? Di che cosa ha veramente bisogno la mia applicazione che non si trovi in uno dei framework esistenti? Posso estendere un framework che gi esiste affinch soddisfi le mie necessit, oppure trovare ci che mi serve da un'altra parte e aggiungerla al framework? Sono sufficientemente preparato sulla costruzione di tale tipo di infrastrutture per essere in grado di raggiungere i requisiti prefissi?
Sulla base delle risposte oneste a questi interrogativi, si potrebbe scoprire che costruirsi da soli il proprio framework non la decisione migliore. Una buona regola su cui mettono la firma molti esperti dell'industria di sviluppo del software questa: "se si tratta di qualcosa relativo al nucleo fondamentale delle proprie attivit, meglio costruirselo da s; ma se il componente software non legato direttamente al nucleo fondamentale della propria attivit, meglio ricercarlo gi pronto". Occorre fare affidamento sui punti di forza della propria squadra e ridurre al minimo i punti deboli.
1.8.2 Barracuda
Il framework di presentazione Barracuda un tipo di architettura Model 2 simile a Struts, che per si spinge ancor pi in l fornendo un meccanismo di notifica degli eventi per il Model. A differenza di un approccio rigidamente JSP, il framework Barracuda ha creato un componente engine per template che dovrebbe garantire flessibilit e possibilit di estensione maggiori. Il framework fa leva sulla separazione tra codice e contenuto fornito dall'approccio XMLC di creare interfacce utente. XMLC un compilatore basato su Java che usa un documento HTML oppure XML per creare classi Java che, una volta mandate in esecuzione, possano ricreare il documento stesso. Le classi Java generate possono essere utilizzate per inserire contenuto dinamico nel documento a runtime agendo su interfacce DOM (Document Object Model). La separazione dei marcatori e della logica dell'applicazione fa s che i web designer possano concentrarsi sui marcatori e i programmatori sul codice. L'unico inconveniente della scelta di tale framework sta nella pi ripida curva di apprendimento da parte degli sviluppatori. L'utilizzo di XMLC e il fatto che vengano create classi Java a partire da documenti HTML o XML possono confondere gli sviluppatori meno esperti. La versione 1.0 del framework Barracuda stata rilasciata non molto tempo fa: maggiori informazioni si possono trovare all'indirizzohttp://barracuda.enhydra.org.
1.8.3 Cocoon
Stefano Mazzocchi ha fatto partire il progetto Cocoon nel gennaio del 1999 come iniziativa open source nell'ambito della ASF. Lo scopo di Cocoon di contribuire a separare lo stile, la logica e le funzioni di gestione per siti web basati su XML. Cocoon fa uso delle tecnologie XML, XSLT (Extensible Stylesheet Language Transformations) e SAX (Simple API for XML) per creare applicazioni server XML, effettuarne il deploy e curarne il mantenimento. Cocoon attualmente giunto alla release 2.0. E previsto il supporto per la maggior parte delle fonti di dati, tra cui RDBMS, LDAP e File Systems. Maggiori informazioni su Cocoon possono essere trovate all'indirizzo http://xml.apache.org/cocoon/.
1.8.4 Expresso
Expresso, prodotto dalla Jcorporate, un framework di sviluppo di applicazioni che fornisce un'infrastruttura a componenti per lo sviluppo di web application basate su database. Il framework Expresso pu essere integrato in Struts, apportando cos caratteristiche per la sicurezza, il mapping object-to-relational, la gestione e la programmazione di lavori in background e molte altre funzioni. Expresso rappresenta pi un complemento a Struts che non un prodotto concorrente. attualmente giunto alla release 4.0. Maggiori informazioni sono reperibili all'indirizzo http://www.jcorporate.com.
WebMacro un framework open source per servlet Java usato da vari grandi siti web. WebMacro utilizza un linguaggio di script leggero che consente la separazione tra aspetto e logica della pagina. WebMacro pu funzionare in modalit standalone o con un servlet container. Attualmente disponibile la release 1.0. Maggiori informazioni su WebMacro si possono trovare all'indirizzo http://www.webmacro.org.
1.8.6 Maverick
Il framework MVC Maverick offre la possibilit di mostrare viste utilizzando le JSP, il linguaggio di script di Velocity oppure l'XSLT. Maverick un'architettura di tipo MVC, ma in realt fornisce un meccanismo di template per le view. Un'ottima funzione di Maverick sta nel fatto di poter utilizzare la reflection sui JavaBeans nello strato di presentazione per creare un'interfaccia DOM, in maniera tale che non sia richiesta la generazione o il parsing di codice XML. Questo permette minor disordine e forse anche migliori prestazioni quando si utilizza XSLT per generare le viste. Maggiori informazioni su Maverick si trovano all'indirizzo http://mav.sourceforge.net.
1.8.7 SiteMesh
SiteMesh un sistema per il layout e l'integrazione di pagine web che rende pi facile creare siti web che necessitano di un look & feel coerente. SiteMesh intercetta le richieste dirette verso una qualsiasi pagina, che sia statica o dinamica, effettua il parsing sul contenuto e genera una pagina finale. Questo processo si basa sul famoso pattern Decorator. [4]
[4]
Il Decorator un pattern strutturale di progettazione di cui si parla nel libro Design Patterns (Addison Wesley), di Gamma, Helm, Johnson e Vlissides, conosciuti con il nomignolo di gang of four ("la banda dei quattro").
SiteMesh costruito con tecnologia Servlet, JSP e XML, il che lo rende adatto alle applicazioni J2EE. Ci nonostante, si afferma anche una facile integrazione con altre tecnologie web, quali CGI. Ulteriori informazioni sono disponibili sul sito http://www.opensymphony.com/sitemesh/.
1.8.9 WebWork
WebWork un piccolo framework per web application che utilizza il design Pull HMVC (Hierarchical Model View Controller). In un'architettura MVC standard, i cambiamenti effettuati al Model vengono inviati alla View. Nel caso di WebWork, sono le viste a prendersi i dati ogni volta che ne hanno bisogno. Fatto interessante, WebWork non sembra essere legato strettamente alle servlet, pertanto pu supportare altri tipi di client quali Swing. Ulteriori informazioni su http://sourceforge.net/projects/webwork/.
CAPITOLO 2
Questo capitolo tratta della relazione tra gli strati presenti nell'architettura e il loro ruolo in un'applicazione. Specifica attenzione viene riservata al tier web, che consente a un'applicazione di comunicare e interagire con i client presenti sul Web. In particolare, questo capitolo si concentra sugli aspetti fisici e logici della progettazione e dell'utilizzo di uno strato web per le applicazioni. Il framework Struts si basa sulle tecnologie Java Servlet e, in misura minore, JavaServer Pages, e necessita quindi di un container web. Per gli sviluppatori Struts, capire il modo in cui il container web elabora le richieste del client fondamentale per un a pi approfondita comprensione dell'infrastruttura stessa. Questo capitolo tratta i vari componenti che fanno parte del container web e le responsabilit di ciascuno di essi.
utilizzando delle varianti di RMI (Remote Method Invocation). Se nello strato intermedio presente un server EJB, il protocollo di comunicazione RMI over IIOP (Internet Inter-ORB Protocol).
Quando presente, l'application tier pu fornire una architettura maggiormente scalabile, pi fault-tolerant e ad alta disponibilit. Uno degli scopi fondamentali per l'uso di un application tier di separare le responsabilit della presentazione da quelle del modello e delle business mie dell'applicazione. Attualmente, molte web application usano server EJB per i loro middle tier. Spesso non utilizzano tutti gli aspetti disponibili dell'architettura J2EE, quali ad esempio i componenti EJB ma, in un server J2EE esistono tante altre caratteristiche vantaggiose su cui si pu fare leva.
Figura 2-2. Il framework Struts viene utilizzato all'interno dello strato web
L'HTTP si basa su un modello richiesta/risposta e quindi esistono due tipi di messaggi HTTP: richiesta e risposta. Il browser apre una connessione verso un server ed effettua una richiesta. Il server elabora la richiesta del client e restituisce una risposta, come illustrato nel processo di Figura 2-3.
Entrambi i tipi di messaggio consistono di una riga di inizio, zero o pi campi di intestazione e di una riga vuota indicante che l finiscono gli header del messaggio. Entrambi i tipi di messaggio possono inoltre contenere un corpo del messaggio (facoltativo). Il formato e la struttura dei messaggi di richiesta e di risposta sono molto simili, ma con alcune differenze. Si analizzer separatamente ciascun tipo di messaggio.
Sebbene esistano diversi metodi HTTP per recuperare dati da un server, i due utilizzati pi frequentemente sono GET e POST. Il metodo GET richiede al server la risorsa indicata dall'URI presente nella request. Se l'URI punta a una risorsa che produce dati, come una servlet, i dati saranno restituiti all'interno del messaggio di risposta. Sebbene il messaggio GET possa passare informazioni nella stringa di query, si usa il metodo POST per passare esplicitamente al server i dati che possono essere usati per l'elaborazione dall'URI della request. L'URI identifica la risorsa che dovrebbe processare la request. Per gli scopi di questa discussione, pu trattarsi di un path sia assoluto che relativo. Una request con un URI non valido, restituir un codice di errore (di solito il 404). La versione del protocollo HTTP della request rende evidente al server a quale versione delle specifiche HTTP si conforma la request. L'esempio seguente illustra la request line di una tipica richiesta GET: GET /index.html HTTP/1.0 possibile eseguire questo esempio aprendo una sessione Telnet su un server che manda in esecuzione un web server. (necessario specificare il nome dell'host e il numero della porta del web server. Per esempio: telnet localhost 80 Poi si pu digitare il comando GET. Sar necessario premere due volte il tasto Enter dopo aver inserito il comando: la prima volta per indicare il termine della request line e la seconda per far capire al server che la request finita. Supponendo l'esistenza di un file denominato index.html nella directory root, sar restituita la response HTML (per la precisione, si avr sempre una risposta, anche se potrebbe non trattarsi di quella che ci si aspetta). Pi avanti nel capitolo si parler ancora di Telnet come strumento di comunicazione con un web server quando si tratteranno redirect e forward. Come gi accennato, una request HTTP pu contenere zero o pi campi header. I campi di intestazione della request consentono al client di passare al server delle informazioni aggiuntive sulla request e sul client stesso. Il formato di uno di questi campi header, sia per la request che per la response, il nome del campo intestazione, seguito dal segno "due punti" (:) e dal valore. Se in un singolo campo header sono specificati valori multipli, essi devono essere separati con virgole (,). La Tabella 2-1 elenca alcuni degli header di request maggiormente utilizzati.
Tabella 2-1. Tipici campi header di una request HTTP
Nome
Scopo Indica i tipi di media accettati nella response. Se non presente alcun campo header Accept, il server riterr che il client accetta tutti i tipi di media. Un esempio di valore per l'header Accept pu essere "image/gif, image/jpeg". Indica i set di caratteri accettati nella response. Se non presente alcun campo header Accept-Charset, il server riterr che il client accetta tutti i set di caratteri. Si ritiene ragionevolmente che il set di caratteri ISO-8859-1 sia accettabile per tutti gli utenti. Simile al campo header Accept, ma restringe ulteriormente i valori di codifica del contenuto accertati dal client. Un esempio di valore per l'header Accept-Encoding pu essere "compress, gzip". Indica la lingua preterita dal client per la response. Esempi di valori per l'header AcceptLanguage possono essere "en-us, de-li, es-us". Indica quale meccanismo di codifica stato applicato al body del messaggio e. pertanto, quale
Accept
Accept-Charset
Accept-Encoding Accept-Language
Content-Encoding meccanismo di decodifica dovr essere usato. Un esempio di valore per l'header ContentEncoding pu essere "gzip". Content-Type Host Referer
Indica il tipo di media del body inviato al ricevente. Un esempio di valore per l'header Content-Type pu essere "text/html; charset=ISO-8859-1". Indica il nome dell'host e il numero della porta della risorsa che si richiede, ricavato dall'URL originale. Un esempio di valore per l'header Host pu essere "www.somehost.com". Permette al client di specificare l'indirizzo (URI) della risorsa dalla quale stato ottenuto l'URI della request. Questo header viene utilizzato principalmente per fini di mantenimento e tracciabilit. Contiene informazioni sul client che ha originato la request. Questo header viene utilizzato principalmente per scopi statistici e per il tracciabilit delle violazioni di protocollo. Un esempio di valore per l'header User-Agent pu essere "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)".
User-Agent
Il corpo del messaggio di una request viene utilizzato per trasportare al server i dati associati alla request. I dati inclusi nel corpo sono diversi dai valori utilizzati nei campi di intestazione, sia in termini di formato che di contenuto. Si pu pensare ai campi header come a dei metadata riguardanti il body del messaggio.
Significato Informativo La request stata ricevuta e viene elaborata Successo L'azione stata ricevuta, capita e accettata con successo. Redirezione Devono essere intraprese ulteriori azioni per il completamento della request. Errore client La request contiene sintassi sbagliata e non pu essere soddisfatta. Errore server Il server non riuscito a soddisfare una request apparentemente valida.
Sono stati definiti non pochi codici di stato. Essi sono inoltre estensibili, il che permette alle applicazioni di estendere il comportamento del server. Se un'applicazione client non riconosce un codice di stato restituito dal server, pu determinare il significato generico della risposta sulla base della prima cifra. La Tabella 2-3 elenca alcuni dei codici di stato pi comuni.
Tabella 2-3. Codici di stato pi comuni nelle response HTTP
Codice 200 302 400 401 403 404 500 OK La request andata a buon fine
Significato Moved temporarily La request risiede momentaneamente su un URI diverso. Se il nuovo URI una location, il campo intestazione Location della response fornir la nuova URL. Questo codice viene di solito utilizzato quando il client subisce una redirezione. Bad request Il server non riuscito a comprendere la request a causa di sintassi errata. Unauthorized La request necessita di autenticazione e/o autorizzazione. Forbidden Il server ha compreso la request ma, per qualche ragione, si rifiuta di soddisfarla. Il server pu o meno rivelare le ragioni per cui rifiuta la richiesta. Not found Il server non ha trovato nulla che corrispondesse all'URI della request. Internal server error Il server si trovato in una condizione inattesa che gli ha impedito di soddisfare la request.
I campi intestazione nella response sono simili nel formato a quelli che si incontrano nel messaggio di request. Consentono al server di inviare al client ulteriori informazioni che non possono essere posizionate nella status line. Questi campi forniscono informazioni sul server e su un ulteriore accesso all'URI contenuto entro la request. Dopo l'ultimo header della response, seguito da una riga vuota, il server pu inserire il corpo del messaggio di risposta. In molti casi, il corpo del messaggio rappresentato da output HTML. La Figura 2-4 illustra una risposta di esempio alla seguente request: GET /hello.html HTTP/1.0
La sessione pu essere anche distrutta prematuramente con un'invocazione al metodo invalidate()presente sull'oggetto session. La sessione consente anche di salvare temporaneamente un insieme di oggetti basandosi su uno schema di coppie chiave/valore, come con l'oggetto request. La sola differenza tra questo schema e quello della request sta nella durata degli oggetti. Dal momento che le sessioni si protraggono per molteplici richieste, gli oggetti immagazzinati nello scope di sessione vivono pi a lungo di quelli esistenti a livello di request. Il framework Struts utilizza ampiamente gli attributi di sessione. Un esempio di un oggetto che pu essere immagazzinato come attributo di sessione l'oggetto Locale dell'utente. Ci consente all'intero framework di avere accesso al locale dell'utente per avere un comportamento coerente con il locale (le convenzioni linguistiche) impostato. Gli oggetti salvati nella sessione di un utente non sono visibili a utenti con una diversa sessione. Il container web non fornisce alcuna sincronizzazione per gli oggetti immagazzinati nella sessione. Se thread multipli tentano di accedere a un oggetto salvato nella sessione per modificarne il valore, garantire la sincronizzazione compito dello sviluppatore. Sebbene la necessit di sincronizzare l'accesso alla sessione sia piuttosto bassa, lo sviluppatore ha la responsabilit di proteggere le risorse. Per esempio, se l'applicazione in questione utilizza i frame, o in presenza di un processo che necessita di tempi lunghi per essere completato, dei thread multipli potrebbero tentare l'accesso alla sessione nel medesimo istante.
web application installate all'interno del container. Ci si verifica quando il container viene avviato per la prima volta. Oltre che lo scope per gli oggetti request e session, la ServletContext permette agli oggetti application di essere immagazzinati e recuperati dall'intera applicazione e di persistere per tutto il ciclo di vita dell'applicazione. Il framework Struts usa lo scope di applicazione per immagazzinare i JavaBeans che devono essere visibili a tutti gli utenti. Normalmente, gli oggetti vengono salvati in questo scope durante l'avvio dell'applicazione e l rimangono finch l'applicazione non si ferma.
A causa dell'ulteriore processo che si verifica, un redirect pi lento di un forward. L'Esempio 2-1 fornisce una servlet di esempio che effettua un'azione di redirect per una pagina JSP denominata result.jsp quando viene prodotta una request per la servlet.
Esempio 2-1 Una servlet java che esegue un'azione di redirect quando riceve una request
package com.oreilly.struts.chapter2Esempios; import import import import import import java.io.IOException; javax.servlet.ServletException; javax.servlet.http.HttpServlet; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; javax.servlet.RequestDispatcher;
/** * Perform a redirect for the page "redirect.jsp" */ public class RedirectServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ redirect( request, response ); }
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ redirect( request, response ); } /** * Set a few URL parameters and objects for the request to see what happens * to them during a redirect. */ protected void redirect(HttpServletRequest req,HttpServletResponse resp) throws ServletException, IOException{ log( "A request arrived for " + req.getServletPath( ) ); // Put some objects into request scope req.setAttribute( "firstName", "John" ); req.setAttribute( "lastName", "Doe" ); String contextPath = req.getContextPath( String redirectStr = contextPath + "/result.jsp?username=foo&password=bar"; log( "redirecting to " + redirectStr ); // Always call the encodeRedirectURL( ) method when perfoming a redirect resp.sendRedirect( resp.encodeRedirectURL(redirectStr) ); } } Quando la servlet dell'Esempio 2-1 riceve una request GET o POST, invoca il metodo redirect() e lo passa agli oggetti HttpServletRequest e HttpServletResponse. Il metodo stabilisce due oggetti String all'interno della request, per dimostrare che non saranno disponibili dopo il redirect, e crea uno String che diverr l'URL per il quale viene detto al client di fare una nuova request. Dopo l'invocazione del metodo encodeRedirectURL()e il passaggio della stringa di redirezione, viene chiamato il metodo sendRedirect()sull'oggetto response.
Tutti gli URL passali al metodo sendRedirect() dovrebbero essere eseguiti tramite il metodo encodeRedirectURL(), in maniera che possa essere incluso l'ID di sessione se il browser non supporta i cookie e c' necessit comunque di registrare ci che avviene nella sessione. Il framework Struts effettua questi processi in maniera automatica all'interno di RequestProcessor durante la normale elaborazione delle azioni.
La pagina JSP verso la quale la servlet effettuer il redirect mostrata nell' Esempio 2-2. Esempio 2-2 La pagina result.jsp
);
<html> <head> <title>Struts Redirect/Forward Esempio</title> </head> <body> <img src="images\tomcat-power.gif"> <br> <% String firstName = (String)request.getAttribute( "firstName" ); if ( firstName == null ){ firstName = "Not found in request"; } String lastName = (String)request.getAttribute( "lastName" ); if ( lastName == null ){ lastName = "Not found in request";
Va notato che gli argomenti relativi a nome e cognome che sono stati impostati nella servlet, non sono stati trovati nella request. Ci accade perch in effetti stata inviata una seconda request per questa pagina. Fortunatamente, possibile osservare ci che succede effettivamente. I parametri URL username e password sono stati inclusi in questo esempio solo per illustrare che sono ancora presenti dopo che si verificato un redirect. Nella stringa di query non si dovrebbero inserire mai delle informazioni riservate, ad esempio password.
Osserviamo la response HTTP che ritorna al client quando viene richiesta la RedirectServlet. Si pu vedere tale risposta utilizzando una sessione Telnet standard. Una connessione HTTP utilizza una semplice socket di rete per comunicare con il server, quindi possibile simulare in parte l'interazione tra un browser e un server usando l'applicazione Telnet. Usando Telnet, si pu stabilire una connessione a un web server collegandosi alla porta sulla quale il server sta in ascolto (normalmente la porta 80 oppure, nel caso di Tomcat, la 8080):
Figura 2-7. Telnet pu essere usalo per vedere gli header della response HTTP
Facendo sempre riferimento alla Figura 2-7, la prima riga della request produce un GET e un percorso verso una risorsa. II path la porzione della request HTTP presente dopo lo hostname e include il carattere "/" che lo precede. La stringa "HTTP/1.0" al termine della request GET rappresenta la versione del protocollo HTTP. La versione 1.1 di HTTP presenta una serie di ottimizzazioni rispetto alla versione 1.0. Ci nonostante, ci sono ulteriori header della request che necessario aggiungere usando la versione 1.1: ci avrebbe reso questo esempio pi complicato del necessario ed per questo che abbiamo usato la 1.0.
Tutto quel che segue la prima riga una response proveniente dal server. L'intero output HTML in basso proviene da Tomcat e indica che la richiesta originale ha effettuato un redirect: il client, pertanto dovrebbe richiedere un nuovo URL. Se si guarda lo header Location della response la terza riga in Figura 2-7 si nota come il server abbia informato il client di quale debba essere l'URL per la nuova request. Ogni parametro URL allegato alla request originale sar presente in quella nuova. Quando il browser produce la nuova request, questi parametri verranno inviati insieme a essa. Questa tecnica che prevede l'uso di Telnet fornisce un modo semplice di interazione con un web server e di vedere quali responsc invia al client. probabile che ora il lettore abbia una migliore comprensione del modo in cui funziona una azione di redirect. Dovrebbe essere anche chiaro perch qualsiasi oggetto presente all'interno della request originale non sia disponibile alla risorsa verso cui avviene il redirect. Questo apparir ini tutta la sua importanza pi avanti nella trattazione del framework Struts. Va notato come, nell'Esempio 2 1, la stringa di redirezione non conteneva esplicitamente il nome di host e la porta di cui aveva bisogno il client per la nuova request. compito del servici container tradurre l'URL relativo in un URL pienamente qualificato da trasmettere indietro al client.
Dal momento che un forward ha luogo completamente all'interno del server e non c' comunicazione di ci al client, le prestazioni sono migliori rispetto a quelle del redirect. E bene tener presesente che ci sono delle differenze nel modo in cui un forward tratta gli URL relativi. L'Esempio 2-3 dovrebbe rendere pi chiaro il tutto.
Esempio 2-3. Una servlet Java che effettua un forward quando riceve una request package
package com.oreilly.struts.chapter2Esempios; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.RequestDispatcher; /** * Perform a forward to the page "redirect.jsp" */ public class ForwardServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ forward( request, response ); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ forward( request, response ); } /** * Set a few URL parameters and objects for the request to see what happens * to them during a forward. */ protected void forward(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ log( "A request arrived for " + req.getServletPath( ) ); // Put some objects into request scope req.setAttribute( "firstName", "John" ); req.setAttribute( "lastName", "Doe" ); String redirectStr = "/result.jsp?username=foo&password=bar"; this.log( "forwarding to " + redirectStr ); RequestDispatcher dispatcher = req.getRequestDispatcher( redirectStr ); dispatcher.forward( req, resp ); } }
Quando la servlet dell'Esempio 2-3 riceve una request di tipo GET o POST, effettua una chiamata al metodo forward() passando ad esso gli oggetti HttpServletRequest e HttpServletResponse. Come nell'esempio del redirect, vengono impostati all'interno della request due oggetti String. Tuttavia, a differenza con quello che accade nell'esempio di redirect, gli oggetti saranno disponibili dopo il forward. In seguito, la servlet crea un path di redirezione verso il quale sar passata la request. Viene creato un RequestDispatcher e viene invocato il metodo forward() presente su di esso. Impiegando la stessa pagina JSP dell'Esempio 2-2, inseriamo nel browser un URL tipo http://localhost:8080/servlet/com.oreilly.struts.chapter2Esempios.ForwardServlet. Quello che apparir nel browser dovrebbe assomigliare a quanto mostrato in Figura 2-9.
Due sono gli aspetti da notare in Figura 2-9. Il primo che i campi di nome e cognome hanno dei valori. Questo accade perch gli oggetti sono stati inseriti nella request prima che si verificasse il forward, e la result.jsp stata in grado di recuperare i valori e utilizzarli nella pagina. Il secondo l'URL nella barra degli indirizzi. Va notato che, mentre in Figura 2-6 veniva mostrato il nuovo URL, esso non cambia in Figura 2-9. Ci mostra ancora una volta come il client non sia al corrente che si sta verificando un forward: al browser non viene notificato che la request stata gestita da una servlet diversa. Il metodo forward() della classe RequestDispatcher pu ricevere invocazioni solo quando nessun output stato invialo al client. Scrivere qualcosa ncll'object della response e poi invocare forward() pu causare da parte del container l'eccezione IllegalStateException.
CAPITOLO 3
E giunto finalmente il momento di presentare il framework Struts. I due capitoli precedenti dovrebbero aver fornito una base che facilita l'apprendimento dei concetti qui contenuti. In questo capitolo si illustra una panoramica sull'infrastruttura Struts. Non si trattano tutte le sue caratteristiche, n si approfondisce troppo, ma si mette chiaramente in luce il modo in cui tutte le varie parti rientrino nelle architetture MVC e Model 2 presentate nel Capitolo 1.I successivi capitoli punteranno invece a scoprire i diversi strati, mettendo in luce i dettagli del framework, espandendo i concetti e la terminologia di base qui presentata. molto importante che il lettore comprnda approfonditamente i concetti fondamentali presentati in questo capitolo: anche se i concetti base del framework struts vi sono gi noti, sarebbe comunque meglio leggere questo capitolo prima di proseguire.
Se vengono immesse credenziali valide per il conto, l'utente viene condotto alla schermata di informazione sul conto, che mostra tutti i conti correnti posseduti dall'utente presso l'istituto bancario e il loro saldo al momento. Per questo semplice esempio, rinunceremo a fornire un ambito e un servizio di sicurezza robusti e sperimentati. Gestire la sicurezza di una web application pu risultare complicato e al momento tale aspetto pu essere tralasciato, per non confondere le idee. Per gli scopi del capitolo, si user una semplice interfaccia Java che contiene un metodo login() per effettuare l'autenticazione degli utenti. Tale interfaccia di autenticazione viene descritta nell'Esempio 3-1 .
Esempio 3-1. L'interfaccia di autenticazione utilizzata nell'applicazione di banking online
package com.oreilly.struts.banking.service; import com.oreilly.struts.banking.view.UserView; /** * Provides methods that the banking security service should implement. */ public interface IAuthentication { /** * The login method is called when a user wants to log in to * the online banking application. * @param accessNumber- The account access number. * @param pin- The account private id number. * @returns a DTO object representing the user's personal data. * @throws InvalidLoginException if the credentials are invalid. */ public UserView login( String accessNumber, String pin ) throws InvalidLoginException; } L'interfaccia IAuthentication contiene un metodo login() molto semplice, che prelevaaccessNumber e pin dalla pagina di login. Se l'autenticazione ha successo, viene restituito un oggetto com.oreilly.struts.banking.view.UserView. Se il login non va a buon fine, viene lanciata una InvalidLoginException. La UserView un semplice JavaBean che pu essere immagazzinato all'interno della vista presentata all'utente e utilizzato per mostrare contenuto specifico per il cliente nell'applicazione. Sebbene non sia del tutto pertinente alla presente discussione, il codice sorgente di UserView sar mostrato pi in l nel capitolo. La classe com.oreilly.struts.banking.service.SecurityService descritta nell'Esempio 3-2: implementa l'interfaccia IAuthentication dell'Esempio 3-1 e permette all'applicazione di autenticare gli utenti. In questo esempio l'autenticazione sar effettuata senza usare security realm, pertanto la classe SecurityService conterr, codificata stabilmente al suo interno, la logica per autenticare gli utenti.
Esempio 3-2. Il servizio di sicurezza utilizzata dall'applicazione di banking online di esempio
package com.oreilly.struts.banking.service; import com.oreilly.struts.banking.view.UserView; /** * Used by the Esempio banking application to simulate a security service. */ public class SecurityService implements IAuthentication { public UserView login( String accessNumber, String pin ) throws InvalidLoginException { // A real security service would check the login against a security realm. // This Esempio is hardcoded to let in only 123/456. if( "123".equals(accessNumber) && "456".equals(pin) ){ /* Dummy a UserView for this Esempio. * This data/object would typically come from the business layer * after proper authentication/authorization had been done. */ UserView userView = new UserView( "John", "Doe" );
userView.setId( "39017" ); return userView; } else { // If the login method is invalid, throw an InvalidLoginException. // Create a msg that can be inserted into a log file. String msg = "Invalid Login Attempt by " + accessNumber + ":" + pin; throw new InvalidLoginException( msg ); } } } Per questa applicazione di esempio, autenticheremo l'utente solo se l' accessNumber immesso corrisponde a "123" e il pin a "456" Se il SecurityService dovesse essere utilizzato in una applicazione reale, dovrebbe effettuare il controllo delle credenziali tramite un qualche tipo realm di sicurezza, come un di database relazionale o un server LDAP.
Effettuato il login con successo, l'utente pu eseguire una tra queste tre azioni: Vedere i dettagli di un conto Trasferire fondi da un conto a un altro, sempre che l'utente abbia due o pi conti Uscire dall'applicazione (logout) La Figura 3-2 mostra la schermata di informazione su conti alla quale si accede dopo un login effettuato con successo. L'utente pu visualizzare informazioni dettagliate su un conto facendo click su tale conto. La Figura 3-3 mostra la schermata di dettaglio per il controllo del conto elencato nella Figura 3-2 .
Lutente pu anche trasferire fondi da un conto ad un altro facendo click sul tasto Transfer vicino al conto a partire dal quale si desidera trasferire i fondi. Dal momento che lo scopo di questo capitolo di familiarizzare con i componenti del framework Struts e non di insegnare il funzionamento corretto di unapplicazione di web banking, la funzionalit di trasferimento dei fondi non sar implementata in maniera completa (ma se il lettore lo desidera, pu provare a farlo come esercizio) Alla fine l'utente pu effettuare il logout dell'applicazione semplicemente con un click sul tasto Logout, uscendo cos dall'applicazione e tornando alla schermata di login.
Il package validator mostrato in Figura 3-4 non rappresenta l'intera serie di classi e interfacce necessarie per il framework Validator. Si tratta solamente delle estensioni specifiche di Struts necessarie per utilizzare il framework Validator con Struts
I componenti del framework non sono organizzati secondo il ruolo che svolgono nel pattern MVC ma, a dire il vero, un po' a casaccio. Probabilmente saranno balzate all'occhio alcune dipendenze circolari mostrate nella Figura 3-4. Questo dovuto pi che altro alla velocit con cui il framework si evoluto, e non a decisioni sbagliate dei progettisti. Per esempio, il package action contiene classi per il controller, alcune classi che sono usate nel dominio della view, e addirittura anche qualcuna che probabilmente sarebbe stata meglio nel package util. Sebbene all'inizio ci possa risultare un po' confuso, non difficile abituarsi a capire dove stanno i diversi componenti. La Tabella 3-1 elenca i package top-level e fornisce una breve descrizione del loro scopo. Alcuni di questi package contengono dei "sottopacchetti", di cui si parler nei successivi capitoli.
Tabella 3-1 Package top-level nel framework Struts
Nome Package
Descrizione Contiene le classi del controller, la ActionForm, la ActionMessages, e diversi altri componenti necessati per il framework. Contiene le classi Action rimanenti, quali la DispatchAction, che pu essere utilizzata o estesa dalla propria applicazione Include le classi di configurazione che sono rappresentazioni presenti in memoria del file di configurazione di Struts. Contiene le classi tag handler per le tag library di Struts. Contiene le classi usati dal framework Tiles. Include classi utilizzate per l'upload e il download dei file dal filesystem locale con un browser Contiene classi di utilit generale utilizzate dall'intero framework. Contiene le classi di estensione, specifiche per Struts, utilizzate dal framework quando si effettua il deploy del validator. Le classi e le interfacce vere e proprie di Validator si trovano nel package commons, che separato da Struts.
Ora che ci si fatta un'idea dei package contenuti all'interno del framework, il momento di analizzare gli strati dell'architettura di Struts.
In Struts, comunque, le responsabilit di controller sono implementate per mezzo di vari componenti, tra i quali un'istanza della classe org.apache.struts.action.ActionServlet.
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception;
Il metodo execute() viene invocato dal controller quando viene ricevuta una request proveniente da un client. Il controller crea un'istanza della classe Action se non ne esiste gi una. Il framework Struts creer solo una singola istanza di ciascuna classe Action della nostra applicazione. Dal momento che c' solamente una istanza per tutti gli utenti, occorre garantire che tutte le classi Action operino correttamente in un ambiente multithread, proprio come si fa quando si sviluppa una servlet. La Figura 3-6 illustra il modo in cui il metodo execute() viene invocato dai componenti del controller.
Sebbene execute() non sia astratto, l'implementazione predefinita restituisce null, quindi sar necessario crearsi la propria implementazione di Action e ridefinire tale metodo.
C' un dibattito sul modo migliore di implementare le classi Action in Struts. Creare una diversa classe Action per ciascuna operazione o collocare varie operazioni sulla medesima Action soggettivo: ciascun approccio presenta vantaggi e svantaggi. Nel Capitolo 5. si analizzer l'azione org.apache.struts.actions.DispatchAction che permette di creare una singola classe Action e di implementare al suo interno svariate azioni simili, quali Create, Read, Update e Delete (CRUD): si creano cosi un numero minore di classi Action al prezzo di un mantenimento leggermente pi scomodo. Per la nostra applicazione di web banking creeremo una particolare classe Action per ciascuna delle azioni possibili all'utente:
package com.oreilly.struts.banking.action; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.struts.action.Action; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import com.oreilly.struts.banking.IConstants; import com.oreilly.struts.banking.service.IAuthentication; import com.oreilly.struts.banking.service.SecurityService; import com.oreilly.struts.banking.service.InvalidLoginException; import com.oreilly.struts.banking.view.UserView; import com.oreilly.struts.banking.form.LoginForm; /** * This Action is called by the ActionServlet when a login attempt * is made by the user. The ActionForm should be an instance of * a LoginForm and contain the credentials needed by the SecurityService. */ public class LoginAction extends Action { public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception { // The ActionForward to return when completed ActionForward forward = null; UserView userView = null; // Get the credentials from the LoginForm String accessNbr = ((LoginForm)form).getAccessNumber( String pinNbr = ((LoginForm)form).getPinNumber( ); );
/* * In a real application, you would typically get a reference * to a security service through something like JNDI or a factory. */ IAuthentication service = new SecurityService( ); // Attempt to log in userView = service.login(accessNbr, pinNbr); // Since an exception wasn't thrown, login was successful // Invalidate existing session if it exists HttpSession session = request.getSession(false); if(session != null) { session.invalidate( ); } // Create a new session for this user session = request.getSession(true); // Store the UserView into the session and return session.setAttribute( IConstants.USER_VIEW_KEY, userView ); forward = mapping.findForward(IConstants.SUCCESS_KEY ); return forward; } }
La LoginAction dell'Esempio 3-3 ottiene le credenziali a partire dalla ActionForm passata come argomento nel metodo execute() La classe ActionForm sar trattata in dettaglio pi avanti in "Componenti della view.
Successivamente viene creato un SecurityService e le credenziali di sicurezza vengono pasate al metodo login(). Se il login va a buon fine, viene creata una nuova HttpSession per l'utente, e la UserView restituita dal metodo login() viene inserita nella sessione. Se l'autenticazione fallisce, viene lanciata una InvalidLoginException. Va notato che non c' un blocco try/catch per la InvalidLoginException nel metodo execute(). La ragione che una delle nuove caratteristiche di Struts 1.1 rappresentata dalle sue possibilit di gestione dichiarativa delle eccezioni, che riduce parecchio la fatica a carico dello sviluppatore per quanto attiene alla gestione delle eccezioni. Con la gestione dichiarativa delle eccezioni in Struts, si dichiarano specificamente nel file di configurazione quali eccezioni possono essere lanciate dalle azioni e quale deve essere il comportamento del framework nei loro confronti:
<action path="/login" type="com.oreilly.struts.banking.action.LoginAction" scope="request" name="loginForm" validate="true" input="/login.jsp"> <forward name="Success" path="/action/getaccountinformation" redirect="true"/> <forward name="Failure" path="/login.jsp" redirect="true"/> </action>
Il mapping dell'azione login qui mostrato mappa il path "/login" alla classe Action com.oreilly.struts.banking.LoginAction. Ogni volta che il controller riceve una request con il path nell'URI che contiene la stringa "/login", sar effettuata un'invocazione al metodo execute()dell'istanza di LoginAction. Il framework Struts utilizza i mapping anche per identificare la risorsa verso cui effettuare il forward dell'utente una volta che l'azione sia stata completata; la configurazione delle mappature delle azioni sar trattata con maggiore dettaglio nel Capitolo 4.
reference al forward dell'azione denominato "Success", ma tutti i mapping delle azioni potevano effettuare reference ai forward dichiarati nella sezione global-forwards. Ecco una sezione global-forwards presa dal file di configurazione dell'applicazione di online banking:
<global-forwards> <forward name="SystemFailure" path="/systemerror.jsp" /> <forward name="SessionTimeOut" path="/sessiontimeout.jsp" /> </global-forwards>
I forward definiti in questa sezione sono pi generali e non si applicano a mapping di azioni specifici. Va notato come ogni forward deve possedere un name e un path, ma l'attributo redirect facoltativo. Se non si specifica un attributo redirect verr utilizzato il suo valore predefinito, uguale a "false", e quindi il framework eseguir un forward. L'attributo path nel forward di un'action pu anche specificare un'altra Action di Struts. Si vedr un esempio a tal proposito nel Capitolo 5. Adesso che si capito come operano ad alto livello i componenti del controller di Struts, giunto il momento di analizzare il successivo tassello del mosaico MVC: il model.
SecurityService. Il componente SecurityService pu essere un EJB session o semplicemente un wrapper per codice JDBC che esegue l'autenticazione. In (entrambi i casi, la LoginAction non conosce il modo in cui
implementato il servizio, e ci non la riguarda. Questo molto utile, poich anche se cambia l'implementazione iti maniera drastica, non saranno necessari cambiamenti al codice, a patto che l'interfaccia IAuthentication non venga modificata e sia implementata dal servizio. Questo approccio favorisce anche il riuso. Immaginiamo di avere un altro tipo di client, come un'interfaccia grafica Swing, che deve essere autenticata: dal momento che la logica incapsulata in un componente separato, e non nella classe Action, si liberi di riusare questo servizio di sicurezza. Occorre sforzarsi di mantenere la business logie fuori dalle classi Action in previsione dei cambiamenti. Nel caso di LoginAction, il metodo login() restituisce un oggetto della classe com.oreilly.struts.banking.view.UserView. Questo un esempio di DTO. L'Esempio 3-4 mostra la UserView utilizzata nell'applicazione di esempio.
Esempio 3-4. Il value object UserView utilizzalo nello strato di presentazione
package com.oreilly.struts.banking.view; import java.util.Set; import java.util.HashSet; /** * A value object that wraps all of the user's security information */ public class UserView implements java.io.Serializable { private String id; private String lastName; private String firstName; // A unique collection of permission String objects private Set permissions = null; public UserView(String first, String last) { this(first, last, new HashSet( )); } public UserView(String first, String last, Set userPermissions) { super( ); firstName = first; lastName = last; permissions = userPermissions; } public boolean containsPermission(String permissionName) { return permissions.contains(permissionName); } public String getLastName() { return lastName; } public void setLastName(String name) { lastName = name; } public String getFirstName() { return firstName; } public void setFirstName(String name) { firstName = name; } public String getId() { return id; }
Per ciascuna pagina HTML in cui sono raccolti dati tramite form, si dovrebbe utilizzare una ActionForm. La stessa ActionForm pu essere utilizzata per pagine multiple, se necessario, a patto che i campi HTML e le propriet di ActionForm abbiano corrispondenza. L'Esempio 3-5 mostra la com.oreilly.struts.banking.form.LoginForm utilizzata nell'applicazione di banking online scelta come esempio.
Esempio3-5. La LoginForm utiliziata nell'applicazione di banking online
package com.oreilly.struts.banking.form; import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.Action; import org.apache.struts.action.ActionError; import org.apache.struts.action.ActionErrors; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionMapping; import org.apache.struts.util.MessageResources; /** * This ActionForm is used by the online banking appliation to validate * that the user has entered an accessNumber and a pinNumber. If one or * both of the fields are empty when validate( ) is called by the * ActionServlet, error messages are created. */ public class LoginForm extends ActionForm { // The user's private ID number private String pinNumber; // The user's access number private String accessNumber; public LoginForm( ) { super( ); resetFields( ); } /** * Called by the framework to validate the user has entered values in the * accessNumber and pinNumber fields. */ public ActionErrors validate(ActionMapping mapping, HttpServletRequest req ){ ActionErrors errors = new ActionErrors( ); // Get access to the message resources for this application. // There's no easy way to access the resources from an ActionForm. MessageResources resources = (MessageResources)req.getAttribute( Action.MESSAGES_KEY ); // Check and see if the access number is missing. if(accessNumber == null || accessNumber.length( ) == 0) { String accessNumberLabel = resources.getMessage( "label.accessnumber" ); ActionError newError = new ActionError("global.error.login.requiredfield", accessNumberLabel ); errors.add(ActionErrors.GLOBAL_ERROR, newError); } // Check and see if the pin number is missing. if(pinNumber == null || pinNumber.length( ) == 0) { String pinNumberLabel = resources.getMessage( "label.pinnumber" ); ActionError newError = new ActionError("global.error.login.requiredfield", pinNumberLabel ); errors.add(ActionErrors.GLOBAL_ERROR, newError); } // Return the ActionErrors, if any. return errors; } /** * Called by the framework to reset the fields back to their default values. */ public void reset(ActionMapping mapping, HttpServletRequest request) { // Clear out the accessNumber and pinNumber fields. resetFields( ); }
/** * Reset the fields back to their defaults. */ protected void resetFields( ) { this.accessNumber = ""; this.pinNumber = ""; } public void setAccessNumber(String nbr) { this.accessNumber = nbr; } public String getAccessNumber( return this.accessNumber; } ) {
public String getPinNumber( ) { return this.pinNumber; } public void setPinNumber(String nbr) { this.pinNumber = nbr; } }
La classe ActionForm fornita dal framework Struts implementa diversi metodi, ma finora i pi importanti sono reset() e validate():
public void reset( ActionMapping mapping, HttpServletRequest request ); public ActionErrors validate( ActionMapping mapping, HttpServletRequest request );
L'implementazione predefinita di entrambi i metodi nella classe ActionForm non esegue alcuna logica predefinita. Sar necessario ridefinire i due metodi nelle classi ActionForm che si utilizzeranno concretamente, come nel caso della LoginForm di Esempio 3-5. Il controller chiama il metodo reset() immediatamente prima di popolare l'istanza di ActionForm con i valori provenienti dalla request, dando alla ActionForm la possibilit di effettuare un reset delle sue propriet allo stato predefinito. Ci molto importante, dal momento che l'istanza del form bean pu essere condivisa tra diverse request, o che ad essa possono avere accesso thread differenti. Ci nonostante, se si utilizza, un'istanza di ActionForm tra pagine multiple, sar il caso di non implementare il metodo reset(), in modo che i valori non subiscano un reset prima che tutte le operazioni di immissione relative a quella istanza siano state completate. Un altro approccio potrebbe essere di implementare un opportuno metodo resetFields( ) da invocarsi a partire dalla classe Action dopo un aggiornamento andato a buon fine sullo strato business. Il metodo validate() viene invocato dal controller dopo che nella ActionForm sono stati inseriti i valori della request. La ActionForm dovrebbe eseguire tutte le possibili validazioni dell'input e riportare al controller ogni eventuale errore riscontrato. La validazione della logica di business dovrebbe essere eseguita a livello di business object, non nella ActionForm. La validazione che si verifica nella ActionForm riguarda solo la presentazione, Nei Capitoli 6 e 7 si tratter in dettaglio dove si debbano collocare determinati tipi di logica di validazione. Il metodo validate() nella LoginForm dell'Esempio3-5 controlla se mancano il numero di accesso e/o il PIN e, nel caso, crea messaggi di errore. Se non vengono generati messaggi di errore, il controller passa ActionForm e svariati altri oggetti al metodo execute(). L'istanza di Action pu poi prelevare le informazioni dall' ActionForm. Si probabilmente notato come il metodo execute() della classe Action contenga un argomento che sempre del tipo ActionForm. Sar necessario effettuare il cast di questo argomento alla corretta sottoclasse, per recuperare le propriet necessarie, come possibile vedere nell'Esempio 3-3. Una volta codificate le classi ActionForm, occorre informare l'applicazione Struts che tali classi esistono e comunicare corrispondenza tra mapping delle azioni e determinate ActionForm: lo si specifica nel file di configurazione. Il primo passo di configurare tutte le ActionForm per la propria applicazione nella sezione formbeans di tale file. La seguente porzione di codice tratta dal file di configurazione dell'applicazione di banking online presa come esempio informa Struts dei tre bean ActionForm usati dall'applicazione:
<form-beans> <form-bean name="loginForm" type="com.oreilly.struts.banking.form.LoginForm"/> <form-bean name="accountInformationForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="accounts" type="java.util.ArrayList"/> </form-bean> <form-bean name="accountDetailForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="view" type="com.oreilly.struts.banking.view.AccountDetailView"/> </form-bean> </form-beans>
L'attributo name di ciascun form bean, deve essere unico, e l'attributo type deve definire una classe Java completamente definita, che estenda la ActionForm di Struts. Il passo successivo di usare uno dei nomi form-bean presi dalla sezione form-bean in uno o pi elementi di action. Il codice che segue mostra il mapping per la LoginAction gi vista nel corso del capitolo:
<action path="/login" type="com.oreilly.struts.banking.action.LoginAction" scope="request" name="loginForm" validate="true" input="/login.jsp"> <forward name="Success" path="/action/getaccountinformation" redirect="true"/> <forward name="Failure" path="/login.jsp" redirect="true"/> </action>
Notare come l'attributo name del mapping del login corrisponda a uno dei nomi presenti nella sezione form-beans. Si fa comunemente riferimento alle istanze della classe ActionForm come ai "form bean". Si user questo termine in tutto il libro per indicare un oggetto di tipo ActionForm.
Una delle nuove caratteristiche di Struts 1.1 viene mostrata nella precedente porzione form-beans. Con le versioni precedenti del framework era necessario estendere sempre la classe ActionForm con una propria sottoclasse, anche se la ActionForm eseguiva comportamenti molto generici. In Struts 1.1 stata aggiunta un nuovo tipo di action form, denominata org.apache.struts.action.DynaActionForm. Questa classe pu essere configurata per il mapping di un'azione e gestir automaticamente i dati passati dal form HTML all'Action. La DynaActionForm in grado di gestire dati in maniera generica poich utilizza una Map per immagazzinare internamente i valori. Il Capitolo 7 tratter in maggiore dettaglio la DynaActionForm. Ma qual la differenza tra una ActionForm e i DTO di cui si parlato in precedenza? Una buona domanda su un argomento che pu ingenerare confusione negli sviluppatori che si avvicinano a Struts. I componenti della view possono utilizzare sia le ActionForm che i DTO per popolare contenuti dinamici. Quando nessuna ActionForm configurata per il mapping, per costruire le viste si possono utilizzare i DTO. E quando definito un form bean per il mapping, esistono svariati modi per gestire l'estrazione dei dati dal bean. Una possibilit di effettuare sempre il wrap con un form bean attorno a uno o pi DTO restituiti dallo strato business e di costringere i componenti della view ad accedere ai dati dei DTO attraverso il form bean. Analogamente, quando un clicnt sottopone una pagina HTML, Struts effettuer un'invocazione ai metodi di set del form bean, che possono spingere i dati di nuovo dentro il DTO, una volta che il metodo di validazione ha completato con successo le sue operazioni. Tutto ci fornisce una singola interfaccia coesiva dalla quale le view possono recuperare e verso la quale possono inviare i dati HTML. Nel Capitolo 7 si parler dei vari vantaggi e svantaggi di questo e altri approcci.
Le pagine JSP costituiscono la maggior parte di ci che deve essere costruito per i componenti della view di Struts. Insieme alle librerie di tag personalizzati e all'HTML, le JSP rendono semplice fornire una serie di viste per un'applicazione. Sebbene le JSP rappresentino la tecnologia usata pi comunemente da organizzazioni e sviluppatori per mostrare contenuti dinamici, esse non costituiscono l'unica possibilit. Molti sviluppatori ritengono che la tecnologia JSP abbia i seguenti problemi: Gli sviluppatori sono liberi di incorporare la logica dell'applicazione all'interno delle pagine JSP, il che pu comportare difficolt di manutenzione delle applicazioni (con JSP 2.0, si possono configurare le pagine JSP affinch non consentano gli scriptlet). Gli sviluppatori devono imparare la sintassi JSP e come si programmano i tag custom (sebbene ci non sar pi vero quando la specifica JSP 2.0 arriver alla versione finale e sar disponibile il nuovo linguaggio di espressione).
Il container deve ricompilare la pagina JSP quando essa subisce qualche cambiamento.
Alcuni sviluppatori non considerano questi aspetti come un problema importante, e molti sono i siti costruiti usando la tecnologia JSP. Tuttavia, per coloro che desiderano delle alternative, esistono altre tecnologie di presentazione utilizzabili con il framework Struts. Un'alternativa piuttosto diffusa la combinazione di XML/XSLT. Ai fini della presentazione delle viste, questo sistema combina la scrvlet controller del framework Struts con XSLT e bean serializzati dai DTO.
<web-app> <taglib> <taglib-uri>/WEB-INF/struts-html.tld</taglib-uri> <taglib-location>/WEB-INF/struts-html.tld</taglib-location> </taglib> <taglib> <taglib-uri>/WEB-INF/struts-logic.tld</taglib-uri> <taglib-location>/WEB-INF/struts-logic.tld</taglib-location> </taglib> </web-app>
Maggiori informazioni sull'installazione e sulla configurazione di Struts per le proprie applicazioni vengono fornite nell'Appendice B. Il passo successivo creare le proprie pagine JSP includendo gli elementi taglib necessari, a seconda delle librerie di cui la pagina avr bisogno:
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
Fatto questo, una volta che i file JAR sono nel CLASSPATH della web application, possibile utilizzare i tag custom nelle proprie pagine JSP. L'Esempio 3-6 mostra l'uso di svariati tag personalizzati di Struts nella pagina login.jsp dell'applicazione di banking online.
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <html:html> <head> <html:base/> <title><bean:message key="title.login"/></title> <link rel="stylesheet" href="stylesheets/login_style_ie.css" type="text/css"> </head> <body topmargin="0" leftmargin="5" marginheight="0" marginwidth="0" bgcolor="#6699FF"> <html:form action="login" focus="accessNumber"> <Tabella border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="#6699FF"> <tr><td> <html:img srcKey="image.logo" width="79" height="46" altKey="image.logo.alt" border="0"/> </td></tr> </Tabella> <Tabella border="0" cellpadding="0" cellspacing="0" width="100%"> <tr><td bgcolor="#000000"> <Tabella border="0" cellpadding="0" cellspacing="0" width="1" height="2"></Tabella> </td></tr> </Tabella> <Tabella border="0" cellpadding="0" cellspacing="0" width="1" height="1"> <tr><td></td></tr> </Tabella> <Tabella> <tr><td></td></tr> </Tabella> <Tabella border="0" cellpadding="0" cellspacing="0" width="590"> <tr><td width="15" height="31"></td><td width="12"></td></tr> <tr> <td width="15"></td> <td width="575" bgcolor="#FFFFFF" colspan="2"> <Tabella cellpadding="0" cellspacing="0" border="0" width="575" height="3"> <tr><td></td></tr> </Tabella> </td> </tr> </Tabella> <Tabella border="0" cellpadding="0" cellspacing="0" width="590" bgcolor="#ffffff"> <tr> <td width="15" bgcolor="#6699FF"></td> <td width="15"></td><td width="379"></td> <td width="15"></td> <td width="15"></td> <td width="15"></td> </tr> <tr> <td bgcolor="#6699FF" width="15"></td> <td></td> <td valign="top">
<Tabella border="0" cellpadding="0" cellspacing="0"> <tr class="fieldlabel"> <td><bean:message key="label.accessnumber"/></td> </tr class="fieldlabel"> <tr> <td><html:text property="accessNumber" size="9" maxlength="9"/></td> <td class="error"><html:errors/></td> </tr> <tr class="fieldlabel"><td height="10"></td></tr> <tr class="fieldlabel"><td><bean:message key="label.pinnumber"/></td></tr> <tr class="fieldlabel"> <td><html:password property="pinNumber" size="4" maxlength="4"/></td> </tr> <tr><td height="10"></td></tr> <tr><td><html:submit styleClass="fieldlabel" value="Login"/></td></tr> <tr><td></td></tr> </Tabella> </td> <td width="151" valign="top"> <html:img srcKey="image.strutspower" altKey="image.strutspower.alt"/> </td> </tr> </Tabella> <%@include file="include/footer.jsp"%> <br> </html:form> </body> </html:html>
Un particolare che dovrebbe subito colpire nella pagina di login dell' Esempio 3-6 che essa non contiene codice Java, e si tratta in gran parte di tag di formattazione HTML e di svariati usi delle librerie di tag di Struts. Proprio qui sta il senso di usare le librerie di lag custom. Non serve programmazione Java, quindi i web designer HTML possono lavorare liberamente sul layout della pagina senza doversi accollare gli aspetti di programmazione. L'altra bella caratteristica che i medesimi tag possono essere utilizzati da molte pagine JSP. Maggiori informazioni sulle librerie di tag si trovano al Capitolo 8. Come si pu immaginare analizzandola pagina JSP dell'Esempio 3-6, la manutenzione e la personalizzazione risultano semplici con l'uso di librerie di tag personalizzati.
label.totalassets=Total Assets label.account=Account label.balance=Available Balance label.description=Description label.amount=Amount label.deposits=Deposits label.withdrawls=Withdrawls label.openingbalance=Opening Balance # Links link.customeragreement=Customer Agreement link.privacy=Privacy link.security=Security link.viewaccountdetail=View Account Detail # Page Titles title.login=Struts Online Banking - Account Login title.accountinfo=Struts Online Banking - Account Information title.accountdetail=Struts Online Banking - Account Detail # Button Labels label.button.login=Login # Error messages global.error.invalidlogin=<li>Invalid Access Number and/or Pin</li> global.error.login.requiredfield=<li>The {0} is required for login</li> # Images image.logo=images/logo.gif image.logo.alt=Struts Online Banking image.logout=images/logout.gif image.logout.alt=Logout image.strutspower=images/struts-power.gif image.strutspower.alt=Powered By Struts image.transfer=images/transfer.gif image.transfer.alt="Transfer Funds" image.clear=images/clear.gif
Tornando alla pagina login.jsp dell'Esempio 3-6, si pu vedere come sono utilizzati i messaggi provenienti dal bundle. Per esempio, la seguente porzione di codice mostra l'uso della chiave title.login dell'Esempio 3-6, inserita tra i tag HTML title nella pagina.
<title><bean:message key="title.login"/></title> org.apache.struts.taglib.bean.MessageTag uno dei vari tag custom presenti in Struts che pu operare sul resource bundle. Le pagine JSP possono recuperare valori dal resource bundle utilizzando il MessageTag
basato su una chiave, come mostrato nella pagina di login dell'Esempio 3-6. La chiave in questo tag deve corrispondere nel bundle a un valore sul lato sinistro del segno uguale. Maiuscole/minuscole sono importanti e il valore deve corrispondere in maniera esatta. L'utilizzo dei resource bundle di messaggi non limita alla localizzazione, ma pu far risparmiare tempo nella manutenzione dell'applicazione. Dovendo cambiare messaggi di testo o etichette in varie parti del sito o dell'applicazione, sar necessario effettuare un solo cambiamento. bene quindi usare i resource bundle anche senza la necessit dell'internazionalizzazione. Con Struts 1.1, si possono definire per un'applicazione svariate MessageResource, con la possibilit ili isolare certi tipi di risorse in hundle separati. Per esempio, potrebbe darsi il caso in cui si salva la risorsa immagine in un bundle e il resto delle risorse in un'altro. Il modo in cui organizzare le resource della propria applicazione resta competenza dello sviluppatore, ma adesso si ha la flessibilit per separarle con un certo criterio: alcune applicazioni effettuano la
separazione Jungo la linea dei componenti, con le risorse inerenti al catalogo tutte messe in un bundle, le risorse collegate a ordini e "carrello della spesa" inserite in un altro bundle e cos via.
3.7 Riepilogo
Il framework Struts fornisce un'implementazione della struttura MVC, specificamente pensata per le web application. Le classi Struts ActionServlet, RequestProcessor e Action costituiscono in componenti del controller, la Action comunica con i componenti model della nostra applicazione; una combinazione di ActionForm, DTO, pagine JSP e librerie di tag costituiscono la view. Il capitolo si concentrato su Struts a un alto livello di astrazione, tralasciando molti dettagli che rendono il framework ancora migliore: come altre valide infrastrutture software, Struts permette di concentrarsi sullo sviluppo della business logic, facendo risparmiare tempo che altrimenti andrebbe dedicato a funzionalit di pi basso livello, quali la notifica delle request e la validazione a livello di campo. Si spera che questa discussione introduttiva abbia interessato il lettore e lo spinga all'approfondimento dei dettagli del framework nel corso del libro.
CAPITOLO 4
Il framework Struts usa due diversi tipi di file di configurazione, comunque tra loro correlati, i quali devono essere configurati in maniera adeguata affinch un'azione funzioni correttamente. Per la popolarit, la flessibilit e la natura autodescrittiva dell'XML, entrambi i tipi di file di configurazione si basano su tale linguaggio. Il descrittore di deploy di una web application denominato web.xml descritto in dettaglio nelle specifiche delle Servlet Java. [1] Questo file di configurazione necessario per tutte le web application, non solo per quelle realizzate con il framework Struts. Esistono per in tale file delle informazioni specifiche per Struts che devono essere configurate quando si costruiscono applicazioni con tale framework.
[1]
Sebbene Struts supporti le specifiche Servlet 2.2, molti container per servlet hanno gi il supporto per la versione 2.3. Nel libro si trattano sia le specifiche 2.2 che le 2.3.
Il secondo file di configurazione che esamineremo quello proprio di Struts, di solito denominato struts-config.xml, ma possibile rinominarlo nel modo che si ritiene pi opportuno. Il file di configurazione di Struts rende possibile la configurazione dichiarativa di molte impostazioni della propria applicazione. II file di configurazione di Struts pu essere considerato quello delle "regole" per la web application.
L'applicazione Storefront implementer un sito di commercio elettronico che si occupa di autoricambi, ma possibile sostituire tali articoli con il tipo di merce che si preferisce, a patto di avere relative immagini e da usare nell'applicazione. Alla fine, l'applicazione Storefront sar un file web archive (WAR) di cui si pu effettuare il deploy in un qualsiasi web container compatibile e che pu essere usata come esempio per molteplici scopi.
Figura 4-2. Le classi java devono essere collocale nelle directory giuste
L'altra directory speciale la WEB-INF/lib, dove si pu effettuare il deploy di file Java Archive (JAR) che poi potranno essere prelevati dal class loader per la web application. Sulla base delle specifiche Servlet 2.3, il class loader della web application deve caricare le classi prima dalla directory WEB-INF/classes, poi dai JAR presenti nella WEB-INF/lib.
Fatti salvi questi particolari requisiti, la struttura delle directory di una web application lasciata alle decisioni dello sviluppatore e dovrebbe essere basata sulle necessit funzionali e non funzionali dell'applicazione. Con web application pi piccole, i file e le risorse possono essere raccolti in poche directory comuni. Con web application pi grandi, per, potr esserci la necessit che ogni componente abbia la sua sottodirectory separata sotto la root dell'applicazione. Ci faciliter sviluppo e manutenzione. La Figura 4-3 mostra la struttura delle directory per la web application Storefront.
Ecco alcuni aspetti da tenere in considerazione nella scelta delle directory per la nostra applicazione Struts: Mantenere separati i componenti facoltativi da quelli indispensabili, per facilitare il deploy parziale. Prendere in considerazione le dimensioni della squadra che effettuer il deploy e la necessit di evitare "contenziosi" per l'uso e il controllo dei file. Prestare attenzione alle dipendenze tra file e risorse, per sfruttare al meglio il riuso e l'inclusione dei file. Rendere le convenzioni su struttura della directory e denominazione dei package quanto pi intuitive ed esplicative possibile. Per molti container, nei nomi di directory e file c' differenza tra maiuscole e minuscole: quindi molto importante essere coerenti con la convenzione (Maiuscolo/minuscolo) che si decide di adottare
I web container sono in grado di caricare un file WAR e di scompattare correttamente le risorse nella struttura richiesta dal container. Il formato WAR utile soprattutto quando necessario che l'applicazione sia distribuita. Tale tipo di file pu inoltre fare parte di un file distribuibile, molto pi grande, detto enterprise archive (EAR). Il Capitolo 16 tratta le pratiche migliori per effettuare il package delle proprie applicazioni utilizzando questi diversi formati.
Parametri di inizializzazione Configurazione delle sessioni Dichiarazioni delle servlet Mapping delle servlet Classi listener per il ciclo di vita dall'applicazione Definizione e mapping di filtri Mapping dei tipi MIME Elenco dei file di benvenuto Pagine di errore Le informazioni di configurazione per la sicurezza non sono obbligatorie tranne quando il servlet container fa parte di un'implementazione J2EE. 1 seguenti elementi non sono richiesti a meno che il container per le servlet usi le pagine JSP o faccia parte di un application server J2EE: Mapping di librerie di tag JNDI References Gran parte delle funzionalit descritte in questo elenco sono state aggiunte nella versione 2.3 delle specifiche per le Servlet e non saranno disponibili per chi usa un container aggiornato alla versione 2.2. Struts versione 1.1 supporta le specifiche Servlet 2.2 e 2.3.
Un documento XML well-formed (ben formato) quello formattato correttamente con tutti i tag aperti e chiusi adeguatamente, tutti gli attributi correttamente indicati, tutte le entit dichiarate e cos via. Quando un documento XML ben formato, per un programma diventa pi facile effettuarne il parsing e inviarlo in rete. Un documento XML valid (valido) quello che dichiara un DTD e aderisce alle regole stabilite in tale DTD. Per ulteriori infbrmazioni, vedi Java & XML d Brett McLaughlin (O'Reilly).
Le seguenti dichiarazioni del DTD mostrano gli elementi top-level che costituiscono il descrittore di deploy per una web application:
<!ELEMENT web-app (icon?, display-name?, description?, distribuTabella?, context-param*, filter*, filter-mapping*, listener*, servlet*, servlet-mapping*, session-config?, mimemapping*, welcome-file-list?, error-page*, taglib*, resourceenv-ref*, resource-ref*, security-constraint*, login-config?, security-role*, env-entry*, ejb-ref*, ejb-local-ref*) >
L'elemento web-app la root del descrittore di deploy di una web application. Gli altri elementi all'interno delle parentesi sono elementi figli, che devono essere collocati all'interno dell'elemento root web-app nel file XML. I simboli vicino agli elementi figli indicano la molteplicit consentita degli elementi figli nel file XML. La Tabella 4-1 fornisce una breve spiegazione dei simboli. L'ordine degli elementi figli dipende dal loro ordine nell'elemento genitore. Per esempio, nella sintassi di web-app, l'elemento servlet viene prima di servlet-mapping, l'elemento servlet-mapping deve venire prima di taglib, e cos via.
Tabella 4-1. Simboli di molteplicit degli elementi figlio in un DTD
Significato Indica che l'elemento figlio deve ricorrere una e una sola volta nell'elemento genitore. Dichiara che lelemento figlio pu ricorrere una o pi volte entro lelemento genitore. Dichiara che l'elemento figlio pu ricorrere zero o pi volte entro l'elemento genitore. Questo simbolo usato abbastanza spesso. Dichiara che l'elemento figlio pu ricorrere zero o una volta entro l'elemento genitore. In altre parole, l'elemento figlio facoltativo. Questo simbolo usato abbastanza spesso.
<!ELEMENT servlet (icon?, servlet-name, display-name?, description?, (servlet-class|jsp-file), init-param*, load-on-startup?, runas?, security-role-ref*) >
Quelli che ci interessano maggiormente sono servlet-name, servlet-class, e init-param. L'elemento servlet-name specifica il nome utilizzato dal descrittore di deploy per definire un reference alla servlet per tutto il resto del file. Quando si configura l'elemento servlet-class per un'applicazione Struts, questo elemento deve specificare una classe completamente definita, che estende la org.apache.struts.action.ActionServlet.
Dal momento che la classe ActionServlet di Struts non astratta, si liberi di usarla, evitando di dover creare una sottoclasse di ActionServlet per la propria applicazione. Con le precedenti versioni di Struts era pi importante estendere ActionServlet con una delle proprie classi perch gran parte delle elaborazioni si verificavano l, e la creazione di sottoclassi consentiva di ridefnire le sue funzionalit con quelle adatte alla propria applicazione. Ma dalla versione 1.1, gran parte delle funzionalit di elaborazione sono state spostate su un'altra classe, che pu essere configurata in maniera dichiarativa (come si vedr pi avanti nel capitolo). Esistono pochi motivi per crearsi una classe ActionServlet, sebbene ci sia libert di farlo. Ecco una parte di web.xml che mostra l'elemento servlet per configurare la classe servlet:
<web-app> <servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>storefront</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
Va notato che il valore dell'elemento servlet-name dentro l'elemento servlet deve corrispondere al valore dell'elemento servlet-name contenuto nel servlet-mapping. Ci indica al web container che la ActionServlet dovrebbe servire tut te le request aventi estensione .do.
La classe ServletContext fornisce una vista esterna dell'ambiente del web container per una servlet. Una servlet pu usare l'oggetto ServletContext per accedere a risorse esterne, per registrare gli eventi in opportuni log, e per salvare attributi e oggetti a cui possono avere accesso istanze di altre servici nello stesso contesto. Si tratta essenzialmente di una risorsa condivisa nello scope dell'applicazione. Dal momento che una servlet associata a una specifica web application, tutte le request che cominciano con uno specifico path (detto context path) vengono indirizzate alla web application associata a tale servlet. Le servlet associate con l'applicazione predefinita presentano una stringa vuota ("") come context path.
Quando un web container riceve la request da un client, deve determinare la web application corretta a cui inviarla: ci possibile grazie alla corrispondenza dell'URL con il context path pi lungo corrispondente a una web application installata. Supponiamo per esempio che su un container siano installate due web application: una si chiama Storefront e si trova fuori dalla root del container nella directory /storefront,mentre la seconda si chiama Storefront_demo e si trova fuori dalla root nella directory /storefront _demo. Se al server arriva la request di un client con l'URL http://www.somehost.com/storefront_demo/login.do, il server la far corrispondere alla web application con la corrispondenza pi stretta, che in questo caso sarebbe la Storefront_demo. Una volta che il container ha determinato il context borretto della web application, deve determinare quale servlet della web application dorrebbe processare la request. Il web container utilizza l'URL della request, meno il context path, per determinare il path da usare per effettuare il mapping della request alla servlet corretta. Per individuare la prima corrispondenza utile, il container utilizza le seguenti indicazioni: 1. 2. 3. 4. Il container cerva una corrispondenza esatta tra path della request e della servlet. Il container tenta in maniera ricofsiva di far corrispondere il prefisso di path pi lungo. Viene selezionata, se presente, la servlet con la corrispondenza pi lunga. Se il path dell'URL contiene un'estensione per esempio, do il servlet container cerca corrispondenza con una servlet che gestisce richieste per tale estensione. L'estensione di definisce come la parte del segmento successiva all'ultimo punto (.). Se nessuna delle precedenti regole .sortisce una corrispondenza, il container cerca di usare una servlet predefinita, se ce ne una configurata. Altrimenti la request restituisce una response di errore. Nella ricerca di corrispondenze, il web container utilizza confronti tra stringhe di tipo case-sensitive (differenza tra Maiuscole e minuscole).
Si fatto cenno al concetto di mapping delle estensioni nel punto 3 delle indicazioni di corrispondenza. C' un altro tipo di mapping che pu essere utilizzato, ed detto path mapping. Un servlet-mapping che utilizzi il mapping dei path, permette a un URL che non ritiene estensioni di effettuare la corrispondenza alla servlet. Utilizzando il precedente mapping alle servlet di Storefront, la seguente porzione di file web.xml illustra come viene configurato il mapping dei path:
inizializzazione sono configurati all'interno dell'elemento servlet utilizzando elementi init-param, come mostrato nel codice seguente del file web.xml:
<web-app> <servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>host</param-name> <param-value>localhost</param-value> </init-param> <init-param> <param-name>port</param-name> <param-value>7001</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>storefront</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
Si pu specificare ogni parametro necessario entro l'elemento init-param, a patto che si tratti di una coppia nome/valore. Per esempio, il precedente descrittore di deploy per la web application comprendeva parametri di inizializzazione per un host e una port. Se si usano gli EJB, questo potrebbe essere il modo per includere le informazioni di connessione del server. Sono consentiti zero o pi elementi init-param. Per la servlet Struts si possono indicare specifici parametri di inizializzazione. Nelle precedenti versioni di Struts, molte opzioni di configurazione che si trovano ora nel file di configurazione di Struts venivano configurate aggiungendo elementi init-param. Sebbene applicazioni costruite e testate con la versione 1.0 continueranno a funzionare anche nella versione 1.1 del framework, potr essere il caso di spostare nella corretta collocazione all'interno del file di configurazione di Struts alcuni dei parametri di inizializzazione ancora specificati nel descrittore di deploy web. Sebbene il framework abbia funzionalit che consentono ai "vecchi" parametri di inizializzazione di funzionare nel file web.xml, qui si tratteranno i parametri della versione 1.1. La Tabella 4-2 identifica i parametri di inizializzazione specificabili in Struts 1.1.
Tabella 4-2 Parametri di inizializzazione per il file web.xml in Struts 1.1
Nome
Scopo/valore predefinito Path relativo al contesto per il file di configurazione predefinito di Struts. Il valore predefinito /WEBINF/struts-config.xml, che funge da applicazione predefinita.
config
Si possono specificare sottoapplicazioni aggiuntive, usando il valore config/ e il prefisso della sottoapplicazione. In questo esempio, il nome di init-param sarebbe config/sub1 e il valore config/sub1 potrebbe essere WEB-INF/struts-sub1-config.xml. Ci indica al controller di caricare la sottoapplicazione sub1 dal file aggiuntivo di configurazione di Struts. Si possono dichiarare tutte le sottoapplicazioni necessarie.
debug
II livello di dettaglio del debug, per la servlet, che controlla quante informazioni vengono registrate in log. un parametro facoltativo, predefinito a 0 se non specificato altrimenti, ovvero al minimo livello di logging possibile. II livello di dettaglio del debug per il Digester, che registra le informazioni a mano a mano che effettua il parsing sui file di configurazione. un parametro facoltativo, predefinito a 0 se non specificato altrimenti, ovvero al minimo livello di logging possibile.
detail
Se questo valore booleano impostato a true, qualsiasi ActionForm che utilizzi classi wrapper Java, come java.lang.Integer, sar impostata a null piuttosto che al valore predefinito per il convertHack tipo della classe. Tale comportamento tipico di Struts 1.0. Il flag facoltativo e il valore predefinito false, che significa che le propriet saranno inizializzate ai loro valori predefiniti Java (p.e.: Integer a 0, Boolean a false, e cos via).
Se si sta supportando un'applicazione Struts 1.0 usando la release 1.1 del framework, il file web.xml potrebbe contenere molti dei parametri di configurazione che vengono attualmente definiti nel file di configurazione di Struts. I parametri si applicano solamente all'applicazione predefinita e, alla line, verranno eliminati nelle release future. E meglio abituarsi a impostarli nel file di configurazione di Struts.
<web-app> <servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>host</param-name> <param-value>localhost</param-value> </init-param> <init-param> <param-name>port</param-name> <param-value>7001</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>storefront</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <taglib> <taglib-uri>/WEB-INF/struts-html.tld</taglib-uri> <taglib-location>/WEB-INF/struts-html.tld</taglib-location> </taglib> <taglib> <taglib-uri>/WEB-INF/struts-bean.tld</taglib-uri> <taglib-location>/WEB-INF/struts-bean.tld</taglib-location> </taglib> <taglib> <taglib-uri>/WEB-INF/struts-logic.tld</taglib-uri> <taglib-location>/WEB-INF/struts-logic.tld</taglib-location> </taglib> </web-app>
L'elemento taglib possiede due sottoelementi: taglib-url e taglib-location. L'elemento taglib-uri specifica un URI che identifica una libreria di tag usata dalla web application; il suo valore pu essere un URI relativo o assoluto e deve trattarsi di un URI valido, ma qui usato come identificatore univoco della libreria di tag. L'elemento taglib-location specifica la collocazione (come risorsa) del file descrittore della libreria di tag. Le librerie di tag di Struts non sono le sole che possono essere dichiarate nel descrittore di deploy della web application. Se si creano le proprie librerie di tag personalizzati, si dovranno creare anche degli elementi taglib per queste.
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html> <body> <logic:forward name="welcome"/> </body> </html> Poi necessario solo aggiungere un elemento welcome-file per la pagina welcome.jsp: <welcome-file-list> <welcome-file>welcome.jsp</welcome-file> </welcome-file-list>
Quando il container utilizzer la risorsa welcome.jsp, effettuer automaticamente un forward verso il forward denominato welcome, definito nella sezione global-forwards. Il forward welcome, a sua volta, invocher l'azione viewsignin.do ottenendo il risultato desi derato.
<web-app> <!-- Other elements go here --> <error-page> <error-code>404</error-code> <location>/common/404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/common/500.jsp</location> </error-page> </web-app>
Quando il codice di stato di un errore viene impostato nella response, il container consulta la lista delle dichiarazioni di error-page per la web application. Se viene trovata una corrispondenza, il container restituisce la risorsa indicata dall'elemento location. Il valore dell'elemento location deve cominciare con un carattere "/" e deve fare riferimento a una risorsa presente all'interno della web application. Se necessario fare riferimento a una risorsa esterna alla web application, si pu usare il metatag HTML Refresh. Per compiere tale operazione, si fa riferimento a un documento HTML statico nell'elemento location, il quale contiene solo la seguente riga:
Per gran parte del capitolo sono state mostrate solamente porzioni dei descrittori di deploy, fondamentalmente per risparmiare spazio e per concentrarci sui vari elementi supportati. ora giunto il momento di mostrare un esempio completo di descrittore di deploy per una web application: l'Esempio 4-4 mostra quello dell'applicazione Storefront.
Esempio 4-4 Un file web xml completo, configurato per Struts 1.1
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>3</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>3</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>storefront</servlet-name> <url-pattern>/action/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <error-page> <error-code>404</error-code> <location>/common/404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/common/500.jsp</location> </error-page> <taglib> <taglib-uri>/WEB-INF/struts-html.tld</taglib-uri> <taglib-location>/WEB-INF/struts-html.tld</taglib-location> </taglib> <taglib> <taglib-uri>/WEB-INF/struts-bean.tld</taglib-uri> <taglib-location>/WEB-INF/struts-bean.tld</taglib-location> </taglib> <taglib> <taglib-uri>/WEB-INF/struts-logic.tld</taglib-uri> <taglib-location>/WEB-INF/struts-logic.tld</taglib-location> </taglib> </web-app>
Ciascuna classe del package config mantiene le informazioni provenienti da una sezione specifica del file di configurazione. Dopo che il file di configurazione ha passato validazione e parsing, il framework Struts usa delle istanze di questi bean per rappresentare versioni "in memoria" delle informazioni che sono state dichiarate nel file di configurazione. Queste classi fungono da container a runtime per le informazioni di configurazione e vengono utilizzate dai componenti del framework come necessario. La classe org.apache.struts.config.ConfigRuleSet mostrata in Figura 4-4 svolge un lavoro leg germente differente, ma collegato al resto: contiene la serie di regole necessarie per effettuare il parsing di un file di configurazione di Struts. Il suo compito sta nel costruire istanze dei JavaBeans di configurazione, cui si fatto cenno nel paragrafo precedente, quando l'applicazione si avvia.
La seguente dichiarazione del DTD di Struts indica che struts-config l'elemento root per il file XML e che possiede otto elementi figli:
<!ELEMENT struts-config (data-sources?, form-beans?, global-exceptions?, globalforwards?, action-mappings?, controller?, message-resources*, plug-in*) >
Si veda la trattazione sul pattern Abstract Factory nel libro Design Pattems; Elementi of Reusable ObjectOriented Software della Gang of Four (Addison Wesley).
Molti produttori forniscono le loro Implementazioni di oggetti datasource. Il linguaggio Java fornisce l'interfaccia javax.sql.DataSource, che deve essere implementata da tutte le altre implementazioni. La maggior parte degli application server e alcuni web container forniscono componenti datasource gi incorporati. Inoltre, tutti i produttori dei pi diffusi database forniscono delle implementazioni di datasource. L'elemento data-sources pu contenere zero o pi elementi data-source:
Per tutta la discussione riguardante gli elementi di configurazione di Struts per il resto del capitolo, si noter un elemento figlio denominato set-property in molti degli elementi principali del file di configurazione. L'elemento set-property specifica il nome e il valore di un'ulteriore propriet di configurazione dei JavaBeans il cui metodo di set verr invocato nell'oggetto che rappresenta l'elemento inglobante. Questo elemento utile specialmente per passare ulteriori informazioni di propriet a una classe estesa di implementazione. L'elemento set-property facoltativo e lo si user solo se necessario passare informazioni aggiuntive a una classe di configurazione. L'elemento set-property definisce tre attributi, compreso l'attributo id, usato raramente. L'attributo property il nome della propriet JavaBeans il cui metodo di set sar invocato. L'attributo value una stringa che rappresenta il valore che sar passato al metodo di set dopo opportuna conversione. Questa sezione fornisce un esempio dell'uso dell'elemento set-property. Lo stesso formato replicato laddove venga dichiarato l'elemento set-property. Gli attributi per l'elemento data-source sono elencati nella Tabella 4-3. Tabella 4-3. Attribuiti dell'elemento data-source Nome Descrizione Attualmente non usato.
id
La classe di implementazione del bean di configurazione che manterr le informazioni della datasource. Se specificato, deve trattarsi di una classe derivata da className org.apache.struts.config.DataSourceConfig, che rappresenta il valore predefinito quando non specificato nulla. Questo attributo facoltativo.
key
L'attributo del contesto della servlet sotto il quale sar salvato la datasource. Questo attributo facoltativo; il valore predefinito Action.DATA_SOURCE_KEY. II nome pienamente qualificato della classe Java di implementazione della datasource. La classe rappresentata da questo valore deve implementare javax.sql.DataSource ed essere configurabile a partire da propriet JavaBeans. Questo attributo facoltativo; il valore predefinito org.apache.struts.util.GenericDataSource. La classe GenericDataSource inclusa nel framework Struts stata deprecata in favore del progetto DBCP (Database Connection Pool) di Jakarta o dell'implementazione fornita dal proprio container.
type
<data-sources> <data-source> <set-property property="autoCommit" value="true"/> <set-property property="description" value="MySql Data Source"/> <set-property property="driverClass" value="com.caucho.jdbc.mysql.Driver"/> <set-property property="maxCount" value="10"/> <set-property property="minCount" value="2"/> <set-property property="user" value="admin"/> <set-property property="password" value="admin"/> <set-property property="url" value="jdbc:mysql-caucho://localhost:3306/storefront"/> </data-source> </data-sources>
Questo codice illustra un elemento data-source configurato per connettersi a un database MySQL tramite un driver JDBC della Caucho Technology, azienda sviluppatrice del container servlet/EJB Resin. Si possono specificare datasource multiple nel file di configurazione, assegnare ciascuna di esse a una chiave unica e accedere a una particolare datasource nel framework tramite la sua chiave. Questo fornisce la possibilit di accedere a database multipli, se necessario. Sebbene le funzionalit di base per le datasource fornite dal framework funzionino piuttosto bene, la nostra applicazione potrebbe aver bisogno di un'implementazione di datasource pi robusta. Esistono svariate implementazioni di datasource piuttosto diffuse, alcune delle quali vengono elencate nella Tabella 4-4.
Tabella 4-4. Implementazioni alternative di datasource Nome Poolman Expresso JDBC Pool DBCP Produttore Open source Jcorporate Open source Jakarta http://www.jcorporate.com http://www.bitmechanic.com/projects/jdbcpool/ http://jakarta.apache.org/commons/index.html URL http://sourceforge.net/projects/poolman/
Il creatore delle librerie open source Poolman non segue pi il progetto. Sebbene funzioni bene e sia sempre disponibile su SourceForge.net, da un po' di tempo che tale implementazione non aggiornala. Ovviamente, visto che open source, sempre possibile apportare i cambiamenti e gli aggiustamenti necessari.
<!ELEMENT form-bean (icon?, display-name?, description?, set-property*, formproperty*) > Per ciascun elemento form-bean si possono inoltre specificare quattro attributi, elencati nella Tabella 4-5.
Tabella 4-5. Attributi dell'elemento form-bean
Descrizione
className
org.apache.struts.config.FormBeanConfig, possibile specificare qui la propria classe, che deve estendere la FormBeanConfig. Questo attributo facoltativo e il framework utilizzer un'istanza della classe FormBeanConfig se non specificato.
Se
utilizzare
il
bean
di
configurazione
standard
dynamic
la classe identificata dall'attributo type un'istanza della org.apache.struts.action.DynaActionForm o una sua sottoclasse, questo valore andrebbe impostato a true, altrimenti sar false. Questo attributo facoltativo; il valore predefinito false. L'attributo stato deprecato e il framework adesso lo determina automaticamente. Un identificatore unico per questo bean, che viene usato per effettuarne il reference in tutto il framework. Questo valore obbligatorio e deve essere unico all'interno della singola sottoapplicazione. Nome pienamente qualificato di una classe Java che estende la ActionForm di Struts. Se tale valore specificato in org.apache.struts.action.DynaActionForm, Struts generer dinamicamente un'istanza della DynaActionForm. Attributo obbligatorio. Occorre fare attenzione quando si configura il valore dell'attributo type: deve trattarsi del nome pienamente qualificato della classe di implementazione di ActionForm. Se si sbaglia a digitare il nome potrebbe risultare assai difficile il debug di questo problema.
name type
Come
accennato
nel
Capitolo
3,
un
form
bean
una
classe
JavaBeans
che
estende
la
classe
<struts-config> <form-beans> <form-bean name="loginForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="username" type="java.lang.String"/> <form-property name="password" type="java.lang.String"/> </form-bean> <form-bean name="shoppingCartForm" type="com.oreilly.struts.order.ShoppingCartForm"/> </form-beans>
</struts-config>
Uno degli elementi form-bean presenti nel codice appena visto utilizza una delle nuove caratteristiche di Struts 1.1, detta action-form dinamici. Degli action form dinamici si fatta menzione nel Capitolo 3 e si parler approfonditamente nel Capitolo 7. Una o pi propriet dinamiche possono essere passate a un'istanza di
org.apache.struts.action.DynaActionForm con l'elemento form-property, supportato solo quando l'attributo type dell'elemento form-bean inglobante org.apache.struts.action.DynaActionForm, o una sua classe derivata. inoltre possibile specificare per ciascun elemento form-property quattro attributi, elencati nella Tabella 4-6.
Tabella 4-6 Attributi dell'elemento form-property
Una rappresentazione sotto forma di stringa del valore iniziale di questa propriet. Se non specificato altrimenti, le primitive saranno inizializzate a zero e gli oggetti a null. Questo attributo non obbligatorio. II nome della propriet JavaBeans della propriet che questo elemento definisce. Si tratta di un attributo obbligatorio. II nome completamente definito della classe Java di implementazione di questa propriet del bean, seguito eventualmente da "[]" per indicare che si tratta di una propriet indicizzata. Questo attributo obbligatorio.
La seguente porzione di codice del fom-bean illustra l'uso dell'elemento form-property: <form-bean name="checkoutForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="firstName" type="java.lang.String"/> <form-property name="lastName" type="java.lang.String"/> <form-property name="age" type="java.lang.Integer" initial="18"/> </form-bean>
Nome
Descrizione
La classe di implementazione del bean di configurazione che manterr le informazioni sull'eccezione. Se specificata, deve essere una classe derivante dalla className org.apache.struts.config.ExceptionConfig. che la classe predefinita quando nessun valore sia specificato.
handler
Il nome completamente definito della classe Java dello handler dell'eccezione che elaborer l'eccezione. Se non specificato alcun valore, sar usata la classe predefinita org.apache.struts.action.ExceptionHandler. Se per questo attributo viene specificata una classe, deve trattarsi di una che derivi dalla ExceptionHandler. Una chiave di messaggio specificata nel resource bundle per questa sottoapplicazione. Questo valore viene usato dall'istanza di ActionError. Il path relativo all'applicazione della risorsa verso la quale effettuare il forward se si verifica l'eccezione. Attributo facoltativo; se non specificato alcun valore, il framework si imposter in maniera predefinita all'attributo input per il mapping dell'azione. L'identificatore del livello di scope nel quale l'istanza di ActionError dovrebbe essere immagazzinato. L'attributo value deve essere request o session. Si tratta di un attributo facoltativo e sar impostato automaticamente a request se non specificato. Il nome completamente definito di classe Java dell'eccezione che deve essere gestita. Questo attributo obbligatorio, perch identifica l'eccezione, dato che il framework non pu conoscerla preventivamente. L'attributo ServletContext che identifica un resource bundle dal quale l'attributo key di questo elemento dovrebbe provenire.
key path
illustra anche i suoi vari elementi figli: <!ELEMENT forward(icon?, display-name?, description, set-property*)>
Come nel caso di exception, gli attributi dell'elemento forward, mostrati nella Tabella 4-8 sono probabilmente pi interessanti degli elementi figli.
Nome
Descrizione La classe di estensione del bean di configurazione che manterr le informazioni di forward. La classe predefinita org.apache.struts.action.ActionForward se non specificato altrimenti. Si tratta di un attributo facoltativo. Viene impostato a true per indicare che la risorsa specificata nell'attributo path dovrebbe essere interpretata come relativa all'applicazione se il path comincia con un carattere "/". Ci al fine di permettere alla risorsa specificata dall'attributo path di risiedere in un'altra sottoapplicazione. Attributo facoltativo, dal valore predefinito pari a false. Un valore unico, utilizzato, per effettuare il reference a questo forward nell'applicazione. Si tratta di un valore obbligatorio. Un URI relativo all'applicazione (se l'attributo contextRelative false) o relativo al contesto (se l'attributo contextRelative true) verso il quale dovrebbe avvenire il forward o il redirect del controllo. Questo attributo obbligatorio e deve cominciare con un carattere "/". Un valore booleano che determina se RequestProcessor debba effettuare un forward o un redirect quando utilizza questo mapping di forward. un attributo facoltativo con valore prenefinito false, che indica l'effettuazione di un forward.
className
contextRelative
name
path
redirect
Ecco un esempio di elemento global-forwards dal codice dell'applicazione Storefront: <global-forwards> <forward name="Login" path="/security/signin.jsp" redirect="true"/> <forward name="SystemFailure" path="/common/systemerror.jsp"/> <forward name="SessionTimeOut" path="/common/sessiontimeout.jsp" redirect="true"/> <forward name="Welcome" path="viewsignin"/> </global-forwards> La classe org.apache.struts.action.ActionForward utilizzata per mantenere le informazioni configurate nell'elemento controller (di cui si parla pi avanti). La classe ActionForward estende ora la org.apache.struts.config.ForwardConfig, per ragioni di retrocompatibilit.
attribute
forward
include
input
name
path
Un parametro di configurazione multiuso, utilizzabile per passare informazioni extra all'istanza dell'action selezionata da questo mapping delle azioni. Il framework basilare non usa questo valore Se viene fornito un parameter valore qui, lo si pu ottenere nella Action invocando il metodo getParameter() sull' ActionMapping passato al metodo execute().
prefix
Utilizzalo per far corrispondere i nomi di parametro della request ai nomi di propriet dei form bean. Per esempio, se tutte le propriet in un form bean cominciano con "pre_", si pu impostare l'attributo prefix in maniera che i parametri della request corrispondano alle propriet. dell' ActionForm. possibile tornire un valore qui solo se l'attributo name specificato Un elenco, separato da virgole, dei nomi dei ruoli di sicurezza a cui permesso invocare l' Action. Quando viene elaborata una request, la classe RequestProcessor verifica che l'utente possieda tra i suoi attributi almeno uno dei ruoli identificati all'interno di questo attributo. L'attributo facoltativo. Usato per identificare lo scope nel quale viene posto il form bean (se request o session), questo attributo pu essere specificato solo se l'attributo name presente. Il valore predefinito session. Utilizzato per far corrispondere i nomi di parametro della request ai nomi di propriet dei form bean. Per esempio, se tutte le propriet in un form bean finiscono in "_foo", si pu impostare l'attributo suffix in maniera che i parametri della request corrispondano alle propriet dell' ActionForm. possibile fornire un valore qui solo se l'attributo name specificato Un
roles scope
suffix
type
org.apache.struts.action.Action. Questo attributo usato per elaborare la request se gli attributi forward o include non sono specificati. Gli attributi forward, include, e type si
escludono l'un con l'altro e ne deve essere specificato uno in maniera esatta. Un valore booleano che indica se l'azione presente deve essere configurata come predefinita per l'applicazione. Se l'attributo impostato true, questa action gestir ogni request che non trattata da altre azioni. Solamente un mapping delle azioni per applicazione pu avere questo valore impostato a true. Questo attributo facoltativo ed impostato inizialmente a false. Si tratta di una buona collocazione in cui impostare una azione predefinita che catturi ogni URL di azione non valido immesso dall'utente. Probabilmente sarebbe stato meglio chiamare questo attributo "default".
nome
pienamente
qualificato
di
classe
Java
che
estende
la
unknown
Un valore booleano indicante se il metodo validate() del form bean, specificato dall'attributo name, validate debba essere invocato prima dell'invocazione al metodo execute() di questa azione.Questo attributo facoltativo preimpostato a true.
<action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action>
La classe org.apache.struts.action.ActionMapping viene usata per rappresentare le informazioni configurate nell'elemento action. La classe ActionMapping estende org.apache.struts.config.ActionConfig per ragioni di retrocompatibilit.
bufferSize
className
contentType
debug
forwardPattern
$A Sostituito dal prefisso di applicazione della presente sottoapplicazione $P Sostituito dall'attributo path dell'elemento forward selezionato $$ Fa si che venga mostrato il segno di valuta del dollaro $x (dove x un qualsiasi carattere non definito sopra) Viene accettato senza
conseguenze, poich riservato per usi futuri Se non specificato altrimenti, il valore predefinito di forwardPattern $A$P, che consistente con il precedente comportamento dei forward inserito nel codice.
inputForward
Si imposta a true quando si vuole che i parametri di input degli elementi action siano i nomi degli elementi forward locali o globali usati per calcolare gli URL definitivi. Si imposta a false (valore predefinito) per trattare i parametri di input degli elementi action come path relativi alla sottoapplicazione per le risorse utilizzate per il modulo di input. Valore booleano che indica se il locale preferito dell'utente immagazzinato nella sessione utente, se non gi presente. un attributo facoltativo, il cui valore predefinito impostato a false. La dimensione massima (in byte) accettabile per un file in upload. Il valore pu essere espresso come un numero seguito da "K", "M", o "G" (vale a dire, rispettivamente, kilobyte, megabyte o gigabyte). un attributo facoltativo, il cui valore predefinito pari a 250M. Il nome pienamente qualificato di classe Java per la classe multipari request-handler che deve essere usata. Si usa quando si caricano file dal filesystem locale di un utente verso il server. un attributo
locale maxFileSize
multipartClass facoltativo il cui valore predefinito la classe DiskMultipartRequestHandler del package org.apache.struts.upload. nocache
Valore booleano indicante se il framework debba impostare header HTTP nocache in ogni risposta. Questo attributo facoltativo; il valore predefinito false, va notato come la lettera "c" nella parola nocache sia minuscola anche se, per coerenza con gli altri attributi qui illustrati, essa avrebbe dovuto essere maiuscola (noCache); ma non stato cosi. Un pattern sostitutivo che definisce il modo in cui gli attributi page dei tag custom che lo usano vengono mappati agli URL relativi al contesto delle risorse corrispondenti. Tale valore pu consistere di una qualsiasi combinazione tra quelli seguenti:
pagePattern
$A Sostituito dal prefisso di applicazione della presente sottoapplicazione $P Sostituito dal valore dell'attributo page $$ Fa si che venga, mostrato il segno di valuta del dollaro $x (dove x un qualsiasi carattere non definito sopra) Viene accettato senza
conseguenze, poich riservato per usi futuri Se non specificato altrimenti, il valore predefinito di pagePattern $A$P, che consistente con il precedente comportamento di valutazione degli URL per gli attributi page, inserito nel codice.
Il nome pienamente qualificato di classe Java della classe request-processor utilizzata per elaborare le request. Il valore qui specificato dovrebbe essere una classe derivata dalla processorClass org.apache.struts.action.RequestProcessor, che rappresenta il valore predefinito. un attributo facoltativo.
tempDir
Specifica la directory temporanea di lavoro usata nell'elaborazione degli upload di file. un attributo facoltativo; il servlet container attribuir, di volta in volta, un valore predefinito per ciascuna web application.
La classe org.apache.struts.config.ControllerConfig rappresenta in memoria le informazioni configurate nell'elemento controller. Ecco come configurare l'elemento controller:
className
factory
key
null
II nome base del resource bundle. Per esempio, se il nome del proprio resource bundle ApplicationResources.properties, si dovrebbe impostare il valore parameter a parameter ApplicationResources. un attributo obbligatorio. Se il resource bundle si trova dentro un package, occorre fornirne in questo attributo il nome pienamente qualificato. Il seguente esempio mostra come configurare elementi message-resources multipli per una singola applicazione. Da notare come per il secondo elemento si sia dovuto specificare l'attributo key, poich ne pu essere immagazzinato solo uno con la chiave predefinita:
<plug-in className="com.oreilly.struts.storefront.service.StorefrontServiceFactory" /> <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in> La classe ValidatorPlugIn presente nel secondo elementoplug-in mostra il modo in cui il framework Struts inizializza il Validator.Il framework Validator viene trattato nel Capitolo 11.
Esempio 4-3. Versione completa di un file di configurazione di Struts <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <data-sources> <data-source> <set-property property="autoCommit" value="true"/> <set-property property="description" value="Resin Data Source"/> <set-property property="driverClass" value="com.caucho.jdbc.mysql.Driver"/> <set-property property="maxCount" value="10"/> <set-property property="minCount" value="2"/> <set-property property="user" value="admin"/> <set-property property="password" value="admin"/> <set-property property="url" value="jdbc:mysqlcaucho://localhost:3306/storefront"/> </data-source> </data-sources> <form-beans> <form-bean name="loginForm" type="com.oreilly.struts.storefront.security.LoginForm"/> <form-bean name="itemDetailForm" type="org.apache.struts.action.DynaActionForm"> <form-property name="view" type="com.oreilly.struts.catalog.view.ItemView"/> </form-bean> </form-beans> <global-exceptions> <exception key="global.error.invalidlogin" path="/security/signin.jsp" scope="request" type="com.oreilly.struts.framework.exceptions.InvalidLoginException"/> </global-exceptions>
<global-forwards> <forward name="Login" path="/security/signin.jsp" redirect="true"/> <forward name="SystemFailure" path="/common/systemerror.jsp"/> <forward name="SessionTimeOut" path="/common/sessiontimeout.jsp" redirect="true"/> </global-forwards> <action-mappings> <action path="/viewsignin" parameter="/security/signin.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" name="loginForm" validate="false" input="/index.jsp"> </action> <action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action> <action path="/signoff" type="com.oreilly.struts.storefront.security.LogoutAction" scope="request" validate="false" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> </action> <action path="/home" parameter="/index.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" validate="false"> </action> <action path="/viewcart" parameter="/order/shoppingcart.jsp" type="org.apache.struts.actions.ForwardAction" scope="request" validate="false"> </action> <action path="/cart" type="com.oreilly.struts.storefront.order.ShoppingCartActions" scope="request" input="/order/shoppingcart.jsp" validate="false" parameter="method"> <forward name="Success" path="/action/viewcart" redirect="true"/> </action> <action path="/viewitemdetail" name="itemDetailForm" input="/index.jsp" type="com.oreilly.struts.storefront.catalog.GetItemDetailAction" scope="request" validate="false"> <forward name="Success" path="/catalog/itemdetail.jsp"/>
</action> <action path="/begincheckout" input="/order/shoppingcart.jsp" type="com.oreilly.struts.storefront.order.CheckoutAction" scope="request" validate="false"> <forward name="Success" path="/order/checkout.jsp"/> </action> <action path="/getorderhistory" input="/order/orderhistory.jsp" type="com.oreilly.struts.storefront.order.GetOrderHistoryAction" scope="request" validate="false"> <forward name="Success" path="/order/orderhistory.jsp"/> </action> </action-mappings> <controller contentType="text/html;charset=UTF-8" debug="3" locale="true" nocache="true" processorClass="com.oreilly.struts.framework.CustomRequestProcessor"/> <message-resources parameter="StorefrontMessageResources" null="false"/> <message-resources key="IMAGE_RESOURCE_KEY" parameter="StorefrontImageResources" null="false"/> <plug-in className="com.oreilly.struts.storefront.service.StorefrontServiceFactory" /> <plug-in className="org.apache.struts.validator.ValidatorPlugIn"> <set-property property="pathnames" value="/WEB-INF/validator-rules.xml,/WEB-INF/validation.xml"/> </plug-in> </struts-config>
Esempio4-6.Parte di un file web.xml che illustra come configurare sottoapplicazioni multiple <servlet> <servlet-name>storefront</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>config/order</param-name> <param-value>/WEB-INF/struts-order-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
Va notato come il valore param-name del modulo applicativo non predefinito mostrato nell'Esempio 4-6 cominci con config. Gli elementi param-name di tutti i moduli applicativi "aggiunti" devono cominciare con config/; l'elemento dell'applicazione "predefinita", invece, contiene il solo valore; config. La parte che segue config/ detta prefisso del modulo applicativoed utilizzata in tutto il framework per intercettare request e restitu le risorse corrette. ire Con l'attuale versione di Struts, quando si usano moduli applicativi multipli supportato solamente il mapping delle estensioni, il mapping dei path non ancora supportato
Occorre prestare particolare attenzione agli attributi di configurazione disponibili nei vari elementi XML di Struts. Alcuni di questi, come visto nel corso del capitolo, hanno un notevole effetto sulle modalit di funzionamento dell'applicazione in un ambiente a moduli multiapplicativi.
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
In versioni precedenti del framework, esistevano alcuni problemi con applicazioni incapaci di avviarsi se non erano in grado di collegarsi al sito Jakarta e di accedere al DTD da l Ci non si verifica pi, visto che Struts ora fornisce copie in locale dei DTD. Alcuni utenti preferiscono specificare un tag SYSTEM DOCTYPE, piuttosto che uno PUBLIC. Ci consente di specificare un path assoluto al posto di uno relativo. Sebbene ci possa risolvere problemi a breve termine, ne crea un numero maggiore a lungo termine. Non sempre possibile garantire la struttura delle directory da un ambiente a un altro e, inoltre, i diversi container agiscono in maniera diversa utilizzando un tag SYSTEM DOCTYPE: forse meglio non usarlo. In ogni caso, se si decide di farlo, dovr trattarsi di qualcosa del genere:
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE struts-config SYSTEM "file:///c:/dtds/struts-config_1_1.dtd"> <struts-config> <!--The rest of the struts configuration file goes next -->
Come si vede, la collocazione del DTD un path assoluto. Se il path dell'ambiente in cui si va ad operare non il medesimo, sar necessario modificare il file XML. Questa la ragione per cui un tale approccio non raccomandato.
CAPITOLO 5
Come si visto nel Capitolo 1, i componenti controller hanno la responsabilit di individuare l'input dell'utente, aggiornare eventualmente il modello del dominio e selezionare la vista che sar presentata al client. Il controller contribuisce alla separazione tra presentazione del model e modello stesso, e ci garantisce maggiore libert di sviluppare presentazioni diversificate basato su un singolo modello del dominio. L'utilizzo di un controller fornisce un punto di controllo centralizzato dove tutte le request dei client vengono elaborate inizialmente. Centralizzare il controllo in tale maniera mette in atto due requisiti del design MVC. Innanzitutto il controller agisce come mediatore/traduttore tra l'input del client e il model, con funzionalit correnti quali controllo di sicurezza, logging o altri servizi importanti per conto di ogni request. In secondo luogo, dal momento che tutte le request sono filtrate attraverso il controller, la view viene disaccoppiata sia dalla business logic che dai componenti di altre viste. La vista restituita al client dipende interamente dal controller. Ci rende le applicazioni molto pi flessibili. Per elaborare le request in entrata, il framework Struts usa una servlet, sebbene faccia affidamento anche su molti altri componenti ausiliari che fanno parte del dominio del controller. I componenti controller di Struts sono stati gi citati brevemente nei precedenti capitoli, ma giunto il momento di analizzarli in maniera approfondita, per capire bene le varie funzioni dei diversi componenti nell'ambito controller.
Esistono anche altri componenti ausiliari che forniscono assistenza a quelli appena illustrati, ma per ora ci concentreremo su quelli mostrati in Figura 5-1.
protected void process(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException { RequestUtils.selectApplication( request, getServletContext( ) ); getApplicationConfig( request ).getProcessor( ).process( request, response ); }
Il metodo process() probabilmente non appare complicato, ma i metodi invocati al suo interno lo sono. Innanzitutto, viene invocato il metodo statico selectApplication() della classe org.apache.struts.util.RequestUtils al quale passata la request corrente e la ServletContext per la web application. Il compito del metodo selectApplication() di selezionare un modulo applicativo che gestisca la request corrente facendo corrispondere il path restituito da request.getServletPath() al prefisso ci ciascun modulo applicativo configurato. Se si utilizza un unico file di configurazione del framework, ci sar una sola applicazione, conosciuta come applicazione predefinita. Per rendere semplice e consistente l'elaborazione delle request all'applicazione predefinita e ai moduli applicativi, l'applicazione predefinita trattata come se fosse un altro modulo applicativo, e quindi ogni request che non contenga un suffisso di applicazione verr indirizzato e gestito dall'applicazione predefinita.
Il metodo selectApplication() immagazzina gli oggettiApplicationConfig e MessageResources opportuni nella request. Ci rende pi facile per il resto del framework sapere quale applicazione e quali componenti dovranno essere utilizzati per la request.
org.apache.struts.action.RequestProcessor, di cui si parler fra breve. Questo nuovo componente del controller stato aggiunto per alleggerire la classe ActionServlet del pesante carico del controller.
Sebbene il framework consenta sembre di estendere la classe ActionServlet, non detto che da tale operazione si possano trarre gli stessi benefici che si avevano con la versione precedente, poich gran parta delle funzionalit sono state spostate nella nuova classe RequestProcessor. Se comunque si desidera continuare a utilizzare la propria versione personalizzata, si pu creare una classe che estenda la ActionServlet e configurare il framework affinch la utilizzi al posto della ActionServlet vera e propria. L'Esempio 5-2 mostra una servlet Java che estende la ActionServlet di Struts e ridefinisce il metodo init().
Esempio 5-2. possibile estendere la classe ActionServlet di Struts affinch effettui un'inizializzazione personalizzata
package com.oreilly.struts.storefront.framework; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import org.apache.struts.action.ActionServlet; import com.oreilly.struts.storefront.service.IStorefrontService; import com.oreilly.struts.storefront.service.StorefrontServiceImpl; import com.oreilly.struts.storefront.framework.util.IConstants; import com.oreilly.struts.storefront.framework.exceptions.DatastoreException; /** * Extend the Struts ActionServlet to perform your own special * initialization. */ public class ExtendedActionServlet extends ActionServlet { public void init( ) throws ServletException { ) first
// Initialize the persistence service try{ // Create an instance of the service interface IStorefrontService serviceImpl = new StorefrontServiceImpl(
);
// Store the service into the application scope getServletContext( ).setAttribute( IConstants.SERVICE_INTERFACE_KEY, serviceImpl ); }catch( DatastoreException ex ){ // If there's a problem initializing the service, disable the web app ex.printStackTrace( ); throw new UnavailableException( ex.getMessage( ) ); } } }
Ridefinire il metodo init() solo un esempio; possibile ridefinire ogni metodo, se necessario. Se si ridefinisce init(), prestare attenzione a invocare il metodosuper.init() affinch si verifichi l'inizializzazione predefinita. Per ora non vi preoccupate di quello che fa il codice dell'Esempio 5-2: lo scopo capire il modo in cui estendere la ActionServlet.
Per configurare il framework affinch utilizzi la nostra sottoclasse di ActionServlet invece di quella standard, sar necessario modificare il file web.xml come segue:
</servlet-class> </servlet>
3.
4.
5.
6. 7. 8.
La Figura 5-2 utilizza un diagramma di sequenza per illustrare gli otto punti principali del processo di inizializzazione della classe ActionServlet.
Ci potrebbe essere la tentazione di impostare svariate servlet controller per una singola applicazione, nel tentativo di ottenere prestazioni migliori. Ci, molto probabilmente, non apporter migliori prestazioni o scalabilit, e viene sconsigliato dagli architetti Sruts. Le servlet sono multithread e permettono a molti clicnt di essere eseguite contemporaneamente. Una singola servlet in grado di soddisfare le richieste contemporanee di molti client. In effetti, il framework Struts 1.1 d per scontalo che ci sia un solo mapping a una servlet: meglio non provare a impostare mapping multipli a pi servlet.
public void process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Wrap multipart requests with a special wrapper request = processMultipart(request); // Identify the path component we will use to select a mapping String path = processPath(request, response); if (path == null) { return; } if (log.isInfoEnabled( )) { log.info("Processing a '" + request.getMethod( ) + "' for path '" + path + "'"); } // Select a Locale for the current user if requested processLocale(request, response); // Set the content type and no-caching headers if requested processContent(request, response); processNoCache(request, response); // General-purpose preprocessing hook if (!processPreprocess(request, response)) { return; } // Identify the mapping for this request ActionMapping mapping = processMapping(request, response, path); if (mapping == null) { return; } // Check for any role required to perform this action if (!processRoles(request, response, mapping)) { return; } // Process any ActionForm bean related to this request ActionForm form = processActionForm(request, response, mapping); processPopulate(request, response, form, mapping); if (!processValidate(request, response, form, mapping)) { return;
} // Process a forward or include specified by this mapping if (!processForward(request, response, mapping)) { return; } if (!processInclude(request, response, mapping)) { return; } // Create or acquire the Action instance to process this request Action action = processActionCreate(request, response, mapping); if (action == null) { return; } // Call the Action instance itself ActionForward forward = processActionPerform(request, response, action, form, mapping); // Process the returned ActionForward instance processActionForward(request, response, forward); }
Come mostra l'Esempio 5-3 succedono molte cose nel metodo processo della classe RequestProcessor. Seguiamo tali eventi nel metodo passo per passo: 1. Viene invocato il metodo processMultipart(). Se il metodo HttpServletRequest un POST e il contentType della request comincia con multipart/form-data, l'oggetto request standard subisce un wrap all'interno di una sua speciale versione del framework Struts che tratta esclusivamente con request multipari. Se il metodo della request un GET o il contentType non multipart, viene restituita la request originale. A meno che l'applicazione supporti l'upload dei file, non necessario preoccuparsi delle funzionalit multipart di Struts. Viene invocato il metodo processPath() per determinare il componente del path daU'URI per la request. Tra l'altro, questa informazione impiegata per selezionare la corretta Action di Struts da invocare. Viene invocato il metodo processLocale() per determinare il locale dell'utente che effettua la request e per immagazzinare un oggetto Locale all'interno dell'oggetto HttpSession. Il locale non sempre ottenuto nella sessione dell'utente, ma dipende dall'attributo locale nell'elemento di configurazione controller. Si veda il Capitolo 4 per maggiori dettagli sugli attributi dell'elemento
2. 3.
controller.
4. Si determina il tipo di contenuto e la codifica opzionale della request attraverso l'invocazione del metodo processContent(). Il content type pu essere configurato nelle impostazioni e anche ridefinito da parte delle JSP: text/html quello predefinito. 5. Viene invocato il metodo processNoCache() per determinare se l'attributo noCache impostato a true. In caso positivo, aggiungere gli opportuni parametri dell'header nell'oggetto response per impedire che le pagine siano sottoposte a cache nel browser. I parametri dell'header comprendono Pragma, CacheControl ed Expires. 6. Successivamente viene invocato il metodo processPreprocess(). un marcatore generico di preelaborazione che, in maniera predefinita, restituisce true. Tuttavia, le sottoclassi possono ridefinire questo metodo ed utilizzare logica condizionale per decidere se continuare l'elaborazione della rcquest. Dal momento che i metodi vengono invocati prima che sia invocata una Action, si tratta di un buon momento per validare se l'utente contiene una sessione valida. Se questo metodo restituisce true, l'elaborazione della request continuer. Se restituisce false, l'elaborazione si fermer. Sta a voi scegliere, tramite opportuna programmazione, se effettuare un redirect o un forward della request: il controller dar per scontato che siate voi a gestire la request e non invier response al client. 7. Si determina la ActionMapping per la request usando le informazioni sul path chiamando il metodo processMapping(). Se non possibile trovare un mapping tramite l'informazione del path, sar restituito un errore al client. 8. Si controlla che ogni ruolo di sicurezza sia configurato per l' Action, invocando il metodo processRoles(). Se ci sono, dei ruoli configurati, viene invocato il metodo isUserInRole() sulla request. Se l'utente non contiene il ruolo necessario, l'elaborazione si concluder a questo punto e sar restituito al clicnt un opportuno messaggio di errore. 9. Si invoca il metodo processActionForm() per determinare se c' una ActionForm configurata per la ActionMapping. Se per il mapping stata configurata una ActionForm, si effettuer un tentativo di trovare un'istanza gi esistente nello scope appropriato. Una volta che la ActionForm sia stata o trovata o creata, viene salvata nello scope opportuno, utilizzando una chiave configurata nell'attributo name per l'elemento ActionForm. 10. Successivamente viene invocato il metodo processPopulate() e, se c' una ActionForm configurata per il mapping, le sue propriet vengono configurate a partire dai valori dei parametri della request. Prima che
11.
12.
13.
14.
15.
le propriet siano popolate a partire dalla request, comunque, viene invocato il metodo reset() sulla ActionForm. Viene chiamato il metodo processValidate() e, se stata configurata una ActionForm e l'attributo validate stato impostato a true per l'elemento action, viene invocato il metodo validate(). Se il metodo validate() individua degli errori, salver un oggetto ActionErrors nello scope della request, e questa verr immediatamente inviata tramite forward alla risorsa specificata nell'attributo input del mapping dell'azione. Se non sono stati individuati errori dal metodo validate() o non c' ActionForm per il mapping delle azioni, l'elaborazione della request continua. Si pu configurare l'elemento controller perch interpreti gli attributi input come forward definiti. Si veda il Capitolo 4 per maggiori informazioni su questa caratteristica. Si determina se configurato per l'action mapping un attributo forward o include. Se cos, si invoca il metodo forward() o include() sulla RequestDispatcher, a seconda di quale configurato. L'elaborazione della request termina a questo punto se uno di questi configurato, altrimenti si continua l'elaborazione della request. Si invoca il metodo processActionCreate() per creare o acquisire un'istanza di Action per elaborare la request. Sar controllata una cache di Action per vedere se l'istanza di Action sia gi stata creata, nel qual caso essa verr utilizzata per elaborare la request. Altrimenti, verr creata una nuova istanza che sar immagazzinata nella cache. Si invoca il metodo processActionPerform(), il quale, a sua volta, invoca il metodo execute() sull'istanza di Action. La chiamata di execute() viene inglobata in un blocco try/catch in maniera che le eccezioni possano essere gestite dalla RequestProcessor. Si invoca il metodo processActionForward() e lo si passa all'oggetto ActionForward restituito dal metodo execute(). Il metodo processActionForward() determina se debba verificarsi un redirect o un forward controllando l'oggetto ActionForward, il quale a sua volta dipende dall'attributo redirect nell'elemento forward.
L'Esempio 5-4mostra una classe RequestProcessor personalizzata che controlla la richiesta per il locale ogni volta e aggiorna la sessione dell'utente se cambiata rispetto a prima. Ci consente all'utente di cambiare la preferenza per un dato locale in qualsiasi momento durante le operazioni dell'applicazione.
Esempio 5-4. La classe RequestProcessor personalizzata che ridefinisce l'elaborazione predefinita del Locale
package com.oreilly.struts.framework; import import import import javax.servlet.http.*; java.util.Locale; org.apache.struts.action.Action; org.apache.struts.action.RequestProcessor;
/** * A customized RequestProcessor that checks the user's preferred locale * from the request each time. If a Locale is not in the session or * the one in the session doesn't match the request, the Locale in the * request is set in the session. */ public class CustomRequestProcessor extends RequestProcessor { protected void processLocale(HttpServletRequest request, HttpServletResponse response) { // Are we conFigurad to select the Locale automatically?
if (!appConfig.getControllerConfig().getLocale( )){ // The locale is conFigurad not to be stored, so just return return; } // Get the Locale (if any) that is stored in the user's session HttpSession session = request.getSession( ); Locale sessionLocale = (Locale)session.getAttribute(Action.LOCALE_KEY); // Get the user's preferred locale from the request Locale requestLocale = request.getLocale( ); // If the Locale was never added to the session or it has changed, set it if (sessionLocale == null || (sessionLocale != requestLocale) ){ if (log.isDebugEnabled( )) { log.debug(" Setting user locale '" + requestLocale + "'"); } // Set the new Locale into the user's session session.setAttribute( Action.LOCALE_KEY, requestLocale ); } } }
Per configurare la CustomizedRequestProcessor per la nostra applicazione, sar necessario aggiungere un elemento controller al lile di configurazione di Struts e includere l'attributo processorClass come mostrato qui:
Determinata la corretta istanza di Action, viene invocato il metodo processActionPerform(). Il metodo processActionPerform() della classe RequestProcessor mostrato nell'Esempio 5-5 .
Esempio 5-5. Il metodo processActionPerform()
protected ActionForward processActionPerform(HttpServletRequest request, HttpServletResponse response, Action action, ActionForm form, ActionMapping mapping) throws IOException, ServletException { try { return (action.execute(mapping, form, request, response)); }catch (Exception e){ return (processException(request, response, e, form, mapping)); } }
Il metodo processActionPerform() ha la responsabilit di invocare il metodo execute() sull'istanza di Action. Nelle precedenti versioni di Struts, la classe Action conteneva solamente un metodo perform(), che stato deprecato in favore di execute() di Struts 1.1. Questo nuovo metodo si rende necessario poich il metodo perform() dichiara di lanciare solamente eccezioni IOExceptions e ServletExceptions. A causa della nuova funzione di gestione dichiarativa delle eccezioni, il framework ha bisogno di ricevere tutte le istanze di java.lang.Exception dalla classe Action. Invece di modificare la firma del metodo perform( ) a scapito della retrocompatibilit, stato aggiunto il metodo execute(). Il metodo execute() invoca il metodo perform(), ma in futuro il metodo perform() sar definitivamente abbandonato. bene usare il metodo execute() invece di perform() in tutte le classi Action. Analizzando il codice sorgente della classe Action, si nota che ci sono due differenti versioni dei metodi execute() e perform(). Una versione prende request e response non HTTP, mentre la seconda contiene le versioni HTTP. Generalmente sar necessaria solo la versione HTTP, a meno che non si utilizzi una servlet non HTTP. Per ora, le versioni non HTTP si limitano a tentare il cast degli oggetti request e response verso le loro controparti HTTP e invocano le versioni HTTP dei rispettivi metodi. Sar necessario estendere la classe Action e fornire un'implementazione del metodo execute(). L'Esempio 5-6 mostra la LoginAction dell'applicazione Storefront.
Esempio 5-6. La classe LoginAction della applicazione Storefront
package com.oreilly.struts.storefront.security; import import import import import import import import import java.util.Locale; javax.servlet.http.*; org.apache.struts.action.*; com.oreilly.struts.storefront.customer.view.UserView; com.oreilly.struts.storefront.framework.exceptions.BaseException; com.oreilly.struts.storefront.framework.UserContainer; com.oreilly.struts.storefront.framework.StorefrontBaseAction; com.oreilly.struts.storefront.framework.util.IConstants; com.oreilly.struts.storefront.service.IStorefrontService;
/** * Implements the logic to authenticate a user for the Storefront application. */ public class LoginAction extends StorefrontBaseAction { /** * Called by the controller when the user attempts to log in to the * Storefront application. */ public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{ // The email and password should have already been validated by the ActionForm String email = ((LoginForm)form).getEmail( );
);
// Log in through the security service IStorefrontService serviceImpl = getStorefrontService( ); UserView userView = serviceImpl.authenticate(email, password); // Create a single container object to store user data UserContainer existingContainer = null; HttpSession session = request.getSession(false); if ( session != null ){ existingContainer = getUserContainer(request); session.invalidate( ); }else{ existingContainer = new UserContainer( ); } // Create a new session for the user session = request.getSession(true); // Store the UserView in the container and store the container in the session existingContainer.setUserView(userView); session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer); // Return a Success forward return mapping.findForward(IConstants.SUCCESS_KEY); } }
Quando viene invocato il metodo execute() nella LoginAction, i valori di e-mail e password vengono recuperati e passati al metodo authenticate(). Se non viene lanciata alcuna eccezione dall'operazione authenticate(), viene creata una HttpSession e un JavaBean contenente informazioni utente viene immagazzinato nella sessione dell'utente. Un bug comune che a volte gli sviluppatori Struts con poca esperienza introducono nelle loro applicazioni di non implementare in maniera corretta il metodo execute(). Se si scrive male o non si implementa esattamente la firma, il metodo non sar mai invocato. Purtroppo non si ricever un errore del compilatore e neanche un errore a runtime che spieghino questo problema, poich la classe Action di Struts, che tutte le classi action devono estendere, ha un metodo execute() predefinito che restituisce null.
La classe UserView contiene semplici propriet quali firstName e lastName che possono essere utilizzate dalla presentazione. A questo tipo di JavaBeans di presentazione si fa comunemente riferimento come value object (VO), ma andrebbero chiamati, in maniera pi formale, data transfer object (DTO) poich si tratta di oggetti utilizzati per trasferire dati da uno strato a un altro. Nella classe UserView mostrata nell'Esempio 5-7i dati vengono trasferiti dal servizio di , sicurezza allo strato di presentazione.
Esempio 5-7. Il DTO della classe UserView
package com.oreilly.struts.storefront.customer.view; import com.oreilly.struts.storefront.framework.view.BaseView; /** * MuTabella data representing a user of the system. */ public class UserView extends BaseView { private String lastName; private String firstName; private String emailAddress; private String creditStatus; public UserView( super( ); } ){
) {
public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getLastName( return lastName; } ) {
) {
public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public void setCreditStatus(String creditStatus) { this.creditStatus = creditStatus; } public String getCreditStatus( ) { return creditStatus; } }
Si parla diffusamente dei DTO nel Capitolo 6. L'Esempio 5-6 usa anche una classe denominata UserContainer, della quale non si danno grandi spiegazioni, quindi non preoccupatevi se vi sfugge qualche cosa. La classe UserContainer un wrapper che ingloba qualsiasi dato che potrebbe normalmente essere inserito direttamente nella sessione utente. Usare tale oggetto, immagazzinando tutto dentro di esso, rende molto pi semplice il recupero e la ripulitura dei dati. buona idea usare un container tipo questo anche per le proprie applicazioni.
ActionForward un wrapper che ingloba la risorsa, in maniera che ci sia minore accoppia mento tra applicazione e risorsa fisica. Quest'ultima specificata solo nel file di configurazione (come attributi name, path, e redirect dell'elemento forward), e non nel codice stesso. La RequestDispatcher pu effettuare o un forward o un redirect per una ActionForward, a seconda del valore dell'attributo redirect.
Per restituire una ActionForward a partire da Action, se ne pu creare una dinamicamente nella classe Action oppure, come accade di solito, possibile utilizzare l'action mapping per individuarne una che sia gi stata preconfigurata nel file di configurazione. Il frammento di codice illustra il modo in cui possibile usare il mapping delle azioni per individuare una ActionForward basandosi sul suo nome logico:
<action input="/security/signin.jsp" name="loginForm" path="/signin" scope="request" type="com.oreilly.struts.storefront.security.LoginAction" validate="true"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action>
Il metodo findForward() della classe ActionMapping innanzitutto effettua una chiamata al metodo findForwardConfig() per vedere se a livello di azione sia specificato un elemento forward con il nome corrispondente. In caso negativo, viene controllata la sezione global-forwards. Quando viene reperita una ActionForward che corrisponde, essa viene restituita alla classe RequestProcessor da parte del metodo execute(). Ecco il metodofindForward() della classe ActionMapping:
public ActionForward findForward(String name) { ForwardConfig config = findForwardConfig(name); if (config == null) { config = getApplicationConfig( ).findForwardConfig(name); } return ((ActionForward) config); }
Se il metodo findForward() non trova un forward che corrisponde all'argomento name, non creer grossi problemi: sar restituito un null e si ricever una pagina vuota, poich non ci sar output da scrivere alla response.
RequestProcessor. L'istanza del log pu essere utilizzata da tutte le request, perch il logger thread-safe e non
mantiene lo stato di client o request particolari. Per lo stato specifico di un client, ip ogni caso, si dovrebbero dichiarare le variabili all'interno del metodo execute(). Qudste variabili locali sono allocate in uno spazio di memoria diverso rispetto a quello delle variabili d'istanza. Ciascun thread che entra nel metodo execute() ha la sua pila per le Variabili locali, quindi non c' possibilit di sovra scrivere lo stato di altri thread.
package com.oreilly.struts.storefront.catalog; import import import import import import import import javax.servlet.http.*; org.apache.struts.action.*; com.oreilly.struts.storefront.framework.exceptions.BaseException; com.oreilly.struts.storefront.framework.UserContainer; com.oreilly.struts.storefront.framework.StorefrontBaseAction; com.oreilly.struts.storefront.catalog.view.ItemDetailView; com.oreilly.struts.storefront.framework.util.IConstants; com.oreilly.struts.storefront.service.IStorefrontService;
/** * An action that gets an ItemView based on an id parameter in the request and * then inserts the item into an ActionForm and forwards to whatever * path is defined as Success for this action mapping. */ public class GetItemDetailAction extends StorefrontBaseAction { public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception { // Get the primary key of the item from the request String itemId = request.getParameter( IConstants.ID_KEY ); // Call the storefront service and ask it for an ItemView for the item IStorefrontService serviceImpl = getStorefrontService( ); ItemDetailView itemDetailView = serviceImpl.getItemDetailView( itemId ); // Set the returned ItemView into the Dynamic Action Form // The parameter name 'view' is what is defined in the struts-config ((DynaActionForm)form).set("view", itemDetailView); // Return the ActionForward that is defined for the success condition return mapping.findForward( IConstants.SUCCESS_KEY ); } }
La classe GetItemDetailAction dell'Esempio 5-8 delega al servizio di Storefront l'effettivo recupero di informazioni sull'articolo. Si tratta si un buon approccio poich la Action non conosce i caratteri intrinseci del servizio
di Storefront o del metodo getItemDetailView(). Pu trattarsi di un oggetto locale che effettua chiamate JDBC, di un session bean che effettua una chiamata remota a un'application server, o di qualche altra implementazione. Se l'implementazione del model cambia (e succeder quando si parler degli EJB nel Capitolo 13), la classe Action sar protetta da tali modifiche. Dato che il servizio di Storefront non a conoscenza del tipo di client che lo usa, pu essere utilizzato anche da client diversi da Struts. Il disaccoppiamento delle classi Action dagli oggetti business verr approfondito nel capitolo successivo.
<action
package com.oreilly.struts.storefront.order; import java.io.IOException; import java.text.Format; import java.text.NumberFormat; import java.util.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.struts.action.*; import org.apache.struts.actions.DispatchAction; import com.oreilly.struts.storefront.service.IStorefrontService; import com.oreilly.struts.storefront.catalog.view.ItemDetailView; import com.oreilly.struts.storefront.framework.UserContainer; import com.oreilly.struts.storefront.framework.util.IConstants; import com.oreilly.struts.storefront.framework.ShoppingCartItem; import com.oreilly.struts.storefront.framework.ShoppingCart; import com.oreilly.struts.storefront.framework.StorefrontDispatchAction; /** * Implements all of the functionality for the shopping cart. */ public class ShoppingCartActions extends StorefrontDispatchAction { /** * This method just forwards to the success state, which should represent * the shoppingcart.jsp page. */ public ActionForward view(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
throws Exception { // Call to ensure that the user container has been created UserContainer userContainer = getUserContainer(request); return mapping.findForward(IConstants.SUCCESS_KEY); } /** * This method updates the items and quantities for the shopping cart from the * request. */ public ActionForward update(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { updateItems(request); updateQuantities(request); return mapping.findForward(IConstants.SUCCESS_KEY); } /** * This method adds an item to the shopping cart based on the id and qty * parameters from the request. */ public ActionForward addItem(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { UserContainer userContainer = getUserContainer(request); // Get the id for the product to be added String itemId = request.getParameter( IConstants.ID_KEY ); String qtyParameter = request.getParameter( IConstants.QTY_KEY ); int quantity; if(qtyParameter != null) { Locale userLocale = userContainer.getLocale( ); Format nbrFormat = NumberFormat.getNumberInstance(userLocale); try { Object obj = nbrFormat.parseObject(qtyParameter); quantity = ((Number)obj).intValue( ); } catch(Exception ex) { // Set the default quantity quantity = 1; } } // Call the Storefront service and ask it for an ItemView for the item IStorefrontService serviceImpl = getStorefrontService( ); ItemDetailView itemDetailView = serviceImpl.getItemDetailView( itemId ); // Add the item to the cart and return userContainer.getCart( ).addItem( new ShoppingCartItem(itemDetailView, quantity)); return mapping.findForward(IConstants.SUCCESS_KEY); } /** * Update the items in the shopping cart. Currently, only deletes occur * during this operation. */
private void updateItems(HttpServletRequest request) { // Multiple checkboxes with the name "deleteCartItem" are on the // form. The ones that were checked are passed in the request. String[] deleteIds = request.getParameterValues("deleteCartItem"); // Build a list of item ids to delete if(deleteIds != null && deleteIds.length > 0) { int size = deleteIds.length; List itemIds = new ArrayList( ); for(int i = 0;i < size;i++) { itemIds.add(deleteIds[i]); } // Get the ShoppingCart from the UserContainer and delete the items UserContainer userContainer = getUserContainer(request); userContainer.getCart( ).removeItems(itemIds); } } /** * Update the quantities for the items in the shopping cart. */ private void updateQuantities(HttpServletRequest request) { Enumeration enum = request.getParameterNames( ); // Iterate through the parameters and look for ones that begin with // "qty_". The qty fields in the page were all named "qty_" + itemId. // Strip off the id of each item and the corresponding qty value. while(enum.hasMoreElements( )) { String paramName = (String)enum.nextElement( ); if(paramName.startsWith("qty_")) { String id = paramName.substring(4, paramName.length( )); String qtyStr = request.getParameter(paramName); if(id != null && qtyStr != null) { ShoppingCart cart = getUserContainer(request).getCart( ); cart.updateQuantity(id, Integer.parseInt(qtyStr)); } } } } }
La classe com.oreilly.struts.storefront.order.ShoppingCartActions contiene i metodi addItem(), update(), e view(). Ciascuno di questi metodi, normalmente, dovrebbe essere inserito in una classe Action separata. Con DispatchAction, possono stare insieme nella stessa classe. Ci sono altri due metodi niella classe ShoppingCartActions di cui non si parlato: updateItems() e updateQuantities(). Si tratta di metodi privati di utilit usati dagli altri metodi di azione all'interno della classe. Non vengono invocati al di fuori di questa classe Action, come si evince dal fatto che non hanno la firma del metodo richiesta. Per utilizzare la classe specializzata DispatchAction, necessario configurare ciascun elemento action che la usa in maniera leggermente diversa rispetto agli altri mapping. L'Esempio 5-10mostra come sia dichiarata nel file di configurazione la classe ShoppingCartActions dell'Esempio 5-9 .
Esempio 5-10 Usando una sottoclasse di DispatchAction occorre specificare l'attributo parameter
<action path="/cart" input="/order/shoppingcart.jsp" parameter="method" scope="request" type="com.oreilly.struts.storefront.order.ShoppingCartActions" validate="false"> <forward name="Success" path="/order/shoppingcart.jsp" redirect="true"/> </action>
L'action mapping /cart mostrato nell'Esempio 5-10 specifica l'attributo parameter e imposta il valore alla stringa letterale "method". Il valore specificato qui diventa importante per l'istanza diDispatchAction quando invocata da un client. La DispatchAction usa questo attributo per determinare quale metodo deve essere invocato nella DispatchAction specializzata. Non ci si limita a chiamare il mapping di azione /cart, ma viene passato un parametro della request; la chiave il valore specificato per l'attributo parameter dal mapping. Il valore di questo parametro di request deve essere il nome del metodo da invocare. Per invocare il metodo addItem() sull'applicazione Storetront, si potrebbe effettuare la chiamata nel modo seguente:
http://localhost:8080/storefront/action/cart?method=addItem&id=2
Il parametro della request denominato method ha un valore di "addltem". Questo viene usato dalla DispatchAction per determinare quale metodo debba essere invocato. Nella sottoclasse DispatchAction deve esserci un metodo che corrisponde al valore del parametro. Il nome del metodo deve corrispondere in maniera esatta, e il metodo deve includere i parametri che di solito si trovano nel metodo execute(). Nella seguente porzione di codice viene illustrata la firma del metodo additem() dell'Esempio 5-9:
public ActionForward addItem( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception; DispatchAction utilizza la reflection per individuare un metodo che corrisponda perfettamente al nome contenuto
nel valore del parametro della request e che contenga lo stesso numero e lo stesso tipo di argomenti. Una volta trovato, il metodo sar invocato e l'oggetto ActionForward sar restituito, proprio come con ogni altra classe Action. Sebbene la DispatchAction utilizzi effettivamente la reflection Java per invocare il metodo corretto, le prestazioni delle API di reflection da Java 1.3 in poi sono talmente migliorate che non si creano problemi in casi come questi. Dal momento che viene usata la reflection, per, il metodo deve essere dichiarato pubblico, o il metodo perform() nella classe astratta DispatchAction non sar in grado di invocarlo. Si possono sempre dichiarare altri metodi privati o pubblici, ma quelli che devono essere invocati dalla DispatchAction devono essere dichiarati pubblici.
request per effettuare un reverse lookup dal resource bundle usando il valore del parametro e per farlo corrispondere a un metodo nella classe. Un esempio aiuter nella comprensione: creiamo innanzitutto una classe che estende LookupDispatchAction e implementa il metodo getKeyMethodMap(). Questo metodo restitui ce un oggetto di tipo java.util.Map s contenente una serie di coppie chiave/valore. Le chiavi di questo Map dovrebbero corrispondere a quelle del resource bundle. Il valore associato a ciascuna chiave in Map dovrebbe essere il nome del metodo della sottoclasse LookupDispatchAction. Questo valore sar invocato quando incluso un parametro della request corrispondente al messaggio del resource bundle per la chiave. Il codice seguente mostra un esempio del metodo ProcessCheckoutAction nell'applicazione Storefront: protected Map getKeyMethodMap( ) { Map map = new HashMap( ); map.put("button.checkout", "checkout" ); map.put("button.saveorder", "saveorder" ); return map; } getKeyMethodMap() per la classe
Per gli scopi di questa trattazione, supponiamo di avere le seguenti risorse nel message resource bundle:
<action path="/processcheckout" input="/checkout.jsp" name="checkoutForm" parameter="action" scope="request" type="com.oreilly.struts.storefront.order.ProcessCheckoutAction"> <forward name="Success" path="/order/ordercomplete.jsp"/> </action>
Poi creiamo una JSP che esegue un POST utilizzando l'azione processcheckout. Con l'header della request sar inviato un parametro di URL action="Checkout". L'Esempio 5-11 mostra la JSP che effettua la chiamata alla action processcheckout. Esempio 5-11 II file checkout.jsp che chiama la ProcessCheckoutAction
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <html:html> <head> <title>Virtual Shopping with Struts</title> <html:base/> <script language=javascript src="include/scripts.js"></script> <link rel="stylesheet" href="../stylesheets/format_win_nav_main.css" type="text/css"> </head> <body topmargin="0" leftmargin="0" bgcolor="#FFFFFF"> <!-- Header Page Information --> <%@ include file="../include/head.inc"%> <!-- Nav Bar --> <%@ include file="../include/menubar.inc"%> <br> Display order summary and take credit card information here <html:form action="/processcheckout"> <html:submit property="action"> <bean:message key="button.checkout"/> </html:submit> </html:form> <br><br> <%@ include file="../include/copyright.inc"%> </body> </html:html>
La chiave per comprendere il funzionamento di tutto ci che il bottone di invio dell'esempio Esempio 5-11 avr un nome "action" e il suo valore sar quello restituito dal tag <bean:message>. Ci appare ancora pi evidente quando si vede il sorgente HTML generato da questa pagina JSP. La seguente porzione di codice mostra il sorgente generato all'interno del tag <html:form>:
<form
RequestUtils. E tuttavia importante prendere dimestichezza con i metodi implementati nella RequestUtils, per
evitare di replicare il comportamento di uno di questi all'interno della propria applicazione e per comprendere appieno ci che l'intero framework fa per vostro conto.
public static void populate( Object bean, Map properties ) throws IllegalAccessException, InvocationTargetException;
getProperty( ) Restituisce una rappresentazione String della propcrty immagazzinata nella variabile con il nome corrispondendente al valore del parametro name. Ecco la firma del metodo getProperty():
public static String getProperty( Object bean, String name ) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException;
Indipendentemente dal tipo di propriet cui fa riferimento l'argomento name, essa sar convertita e restituita sotto forma di String. getArrayProperty( ) Restituisce il valore della propriet dell'array specificato nello specifico bean come array String. Ecco la firma del metodo getArrayProperty():
public static String[] getArrayProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException;
Sebbene il linguaggio Java fornisca reflection e introspection come parte delle sue API fondamentali, la classe BeanUtils fornisce wrapper di comodit per inglobare tali API.
L'altra classe usata dal framework Struts la PropertyUtils. Attualmente, viene usato solo il metodo getProperty() di questa classe. Il metodo getProperty() della classePropertyUtils restituisce il valore della propriet specificata, senza cercare di convertirne il tipo. Ecco la firma del metodo: public static Object getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException;
Gran parte del codice della classe PropertyUtils era in origine implementato nella BeanUtils. stato collocato in una classe a parte perch BeanUtils diventava troppo grande.
CAPITOLO 6
In questo capitolo saranno trattati gli elementi che costituiscono, all'interno dell'applicazione Struts, la componente model. Il model rappresenta i dati di elaborazione di un'applicazione e dovrebbe rispecchiare fedelmente le entit e i processi elaborativi del mondo reale che esso rappresenta. Si esamineranno inoltre ruoli e responsabilit dei componenti del model all'interno di Struts prestando particolare attenzione alla costruzione di un'implementazione dell'applicazione Storefront corretta dal punto di vista dell'architettura. Si porr l'accento sull'utilizzo di un'infrastruttura di persistenza che possa essere integrata facilmente e senza problemi in un'applicazione basata su Struts.
If you find yourself importing packages or classes from the Struts framework into your model, you are violating this principle. Coupling a lower layer to an upper one will make maintenance, reuse, and future enhancements more difficult. Before we get into the details of designing and building a model for a Struts application, let's look at the different types of models and how each one is relevant to a project.
The term "model" has many different meanings. In very general terms, a model is a representation of some aspect of reality, such as a shop where products are bought and sold, an auction house where bids are placed, or a way to predict how a weather storm will move. All of these Esempios are based on real concepts. The main purpose of creating a model is to help understand, describe, or simulate how things work in the real world. In software development, the term "model" is used to indicate both the logical representation of real-world entities and the physical creation of classes and interfaces that programs can use. The first step, however, always should be to perform a thorough analysis of the problem domain. Once use-cases are complete, the next step should be to develop a conceptual model. 6.1.1.1 The conceptual model During analysis of the problem domain, a conceptual model should be developed based on the real-life entities within the problem space. The entities in a conceptual model have less to do with the software components of the system and more to do with the physical entities that are fundamental to the business. The conceptual model usually illustrates the concepts, the relationships between them, and the attributes that belong to each concept. The behavior usually is not represented in this type of model. The conceptual model is developed from a set of use-cases for the system. The purpose of building the model is to help identify the entities that most likely will become classes in the design stage and to help better understand the problem domain. Figura 6-2 illustrates a conceptual model for the Storefront application. Notice that only relationships and the attributes for the entities are shown; no methods are specified. Figura 6-2. The Storefront conceptual model
If you are familiar with Entity-Relationship (E-R) diagrams, you shouldn't be too confused by the conceptual model. They are very similar.
The value of a conceptual model is that it clearly shows the entities that are used by the problem domain. Everyone involved in the business, technical or not, should be able to look at the conceptual model and make sense of it. They also should be able to quickly point out problems with the model. For Esempio, maybe an item can't belong to more than one catalog at a time. By examining the conceptual model, someone would be able to point this out, and the analysts could make the change early. The later in the design and development cycle that a change is required, the more costly that change becomes.
Il diagramma delle classi nella Figura 6-3 mostra i business object usati nell'applicazione Storefront. Per semplificare, sono state evidenziate solo relazioni e attributi. Il modo in cui si arriva a una progettazione accurata delle applicazioni va oltre gli scopi di questo libro. C' gran numero di ottimi libri sull'analisi e la progettazione. Uno di questi Applying UML and Pattems di Craig Larman (Prentice Hall). Tipo e struttura dei business object dipendono ovviamente dal dominio della propria applicazione e non esistono due applicazioni che siano esattamente uguali. Persino all'interno di una singola applicazione si pu capire intuitivamente che le richieste mutano. Tuttavia, un'analisi accurata del dominio del problema complessivo e una progettazione rigorosa mettono al riparo la propria applicazione da cambiamenti inaspettati.
appartenente al dominio dell'elaborazione. Concetti quali articoli, ordini e clienti sono tutti business objeet del dominio dell'applicazione Storefront. I business object sono costituiti da stato e comportamento. Il business object OrderBO, ad esempio, contiene le informazioni relative a un ordine di acquisto di un singolo cliente, incluso prezzo, tasse e stato dell'ordine. Un OrderBO deve anche sapere chi il cliente ed essere in grado di trasmettere queste informazioni. Per poter essere considerata un business object importante che una classe possegga le informazioni di stato e comportamento. Esaminiamo altre caratteristiche dei business object.
l'oggetto invocante non sar coinvolto, dal momento che il client programma utilizzando l'interfaccia List e non la classe di implementazione. A questo punto si dovrebbe avere cosipevolezza dell'importanza che i business object rivestono all'interno di una organizzazione. Sono infatti presenti in tutte le applicazioni (eccetto le pi banali). Dovrebbe anche essere chiaro che contengono stato e comportamento e che, nella maggior parte dei casi, questi ultimi agiscono sui dati. Ci si potrebbe chiedere dunque dove ha origine lo stato e dove viene conservato quando l'applicazione viene terminata: il momento di parlare di persistenza degli oggetti.
6.3 Persistenza
Generalmente con il termine persistenza ci si riferisce al fatto che i dati inseriti in un'applicazione, sia da un utente umano che da altri mezzi, avranno un'esistenza che va oltre il ciclo di vita dell'applicazione. L'informazione sopravviver agli arresti dell'applicazione o allo spegnimento della macchina. Questo importante per un'organizzazione dal momento che vi sempre la necessit di dati persistenti qualsiasi sia la grandezza dell'organizzazione stessa.
Non possibile in questa sede discutere il mapping degli oggetti in un database relazionale per motivi di spazio. Basti al momento sapere che vi sono diversi approcci. Fortunatamente sono disponibili numerose risorse e punti di riferimento per la soluzione di questi problemi. Il documento di Scott Ambler "Mapping Objects to Relational Databases" costituisce la pi importante fonte di informazioni sull'argomento. (Si veda http://www. ambysoft. com/mappingObjects.pdf). Come si vedr in seguito, vi sono molte infrastrutture per il mapping tra database a oggetti e relazionali (object-torelational mapping, ORM) che rendono questo lavoro molto pi semplice per lo sviluppatore Java. Queste infrastrutture
non eliminano completamente la necessit di una buona comprensione del problema, ma possono evitare molti dei compiti sgraditi agli sviluppatori.
package com.oreilly.struts.storefront.businessobjects; /** * An abstract superclass that many business objects will extend. */ abstract public class BaseBusinessObject implements java.io.Serializable { private Integer id; private String displayLabel; private String description; public Integer getId( return id; } ) {
public void setId(Integer id) { this.id = id; } public void setDescription(String description) { this.description = description; } public String getDescription( return description; } ) {
public void setDisplayLabel(String displayLabel) { this.displayLabel = displayLabel; } public String getDisplayLabel( return displayLabel; } }
La BaseBusinessObject evita che ogni business object debba dichiarare queste propriet comuni. Se esiste business logic comune, possibile inserire anche essa in questa sede. L'Esempio 6-2 mostra il business object OrderBO che rappresenta un ordine d'acquisto di un cliente nell'applicazione Storefront. Non c' niente di particolare circa la classe OrderBO; solo un normale oggetto JavaBeans. Oltre al metodo recalculatePrice(), la classe offre solo i metodi di get e di set relativamente alle propriet dell'ordine.
Esempio 6-2. L'oggetto OrderBO rappresentante un ordine di un cliente
) {
package com.oreilly.struts.storefront.businessobjects; import import import import import java.math.BigDecimal; java.sql.Timestamp; java.util.Iterator; java.util.List; java.util.LinkedList;
/** * The OrderBO, which represents a purchase order that a customer * has placed or is about to place. */ public class OrderBO extends BaseBusinessObject{ // A list of line items for the order private List lineItems = new LinkedList( ); // The customer who placed the order private CustomerBO customer; // The current price of the order private double totalPrice; // The id of the customer private Integer customerId; // Whether the order is in process, shipped, canceled, etc. private String orderStatus; // The date and time that the order was received private Timestamp submittedDate; public OrderBO( Integer id, Integer custId, String orderStatus, Timestamp submittedDate, double totalPrice ){ this.setId(id); this.setCustomerId(custId); this.setOrderStatus(orderStatus); this.setSubmittedDate(submittedDate); this.setTotalPrice(totalPrice); } public void setCustomer( CustomerBO owner ){ customer = owner; } public CustomerBO getCustomer( return customer; } public double getTotalPrice( return this.totalPrice; } ){ ){
private void setTotalPrice( double price ){ this.totalPrice = price; } public void setLineItems( List lineItems ){ this.lineItems = lineItems; } public List getLineItems( return lineItems; } ){
public void addLineItem( LineItemBO lineItem ){ lineItems.add( lineItem ); } public void removeLineItem( LineItemBO lineItem ){ lineItems.remove( lineItem ); } public void setCustomerId(Integer customerId) { this.customerId = customerId; } public Integer getCustomerId( return customerId; } ) {
public void setOrderStatus(String orderStatus) { this.orderStatus = orderStatus; } public String getOrderStatus( return orderStatus; } ) {
public void setSubmittedDate(Timestamp submittedDate) { this.submittedDate = submittedDate; } public Timestamp getSubmittedDate( return submittedDate; } private void recalculatePrice( double totalPrice = 0.0; ){ ) {
if ( getLineItems( ) != null ){ Iterator iter = getLineItems().iterator( ); while( iter.hasNext( ) ){ // Get the price for the next line item and make sure it's not null Double lineItemPrice = ((LineItemBO)iter.next()).getUnitPrice( ); // Check for an invalid lineItem. If found, return null right here. if (lineItemPrice != null){ totalPrice += lineItemPrice.doubleValue( ); } } // Set the price for the order from the calcualted value setTotalPrice( totalPrice ); } } }
Non si riporteranno qui tutti i business object, in quanto la loro implementazione analoga alla classe OrderBO.
Quando si progettano i propri business object, si pu trascurare il modo in cui sono mappati nel database. Ci sar tempo a sufficienza per farlo. Non ci si deve preoccupare di usare tecniche orientate agli oggetti come ereditariet e polimorfismo, proprio come si farebbe con qualsiasi altro modello di oggetti. BaseBusinessObject nell'Esempio 6-1 non sar effettivamente mappata in una tabella di database ma lo saranno le sue propriet con le rispettive sottoclassi. Anche se la maggior parte delle infrastrutture che operano il mapping della persistenza supportano diversi approcci al mapping dell'ereditariet all'interno di un database, se si aggiungono le propriet a ogni tabella possono essere eseguite meno operazioni di join di SQL, migliorando la performance.
Le definizioni della tabella nella Figura 6-4 si commentano da s. Ci sono per parecchi elementi che meritano il nostro interesse e che devono essere evidenziati. Il primo che ad ogni tabella, eccetto che a CATALOGITEM_LNK, stato assegnato un identificatore di oggetto (OID). Gli OID semplificano la navigazione tra gli oggetti e non dovrebbero avere alcun significato per l'applicazione. I valori che si basano sulla semantica di elaborazione sono soggetti a cambiamenti e basare le proprie chiavi su valori che cambiano molto problematico. Nel mondo dei database, l'utilizzo di una strategia basata su OID chiamato utilizzo di chiavi surrogate.
Per generare lo schema per il modello di dati illustrato nella figura Figura 6-4, necessario creare il DDL. Il DDL SQL usato per creare le entit fisiche nel database; quello di Storefront usato per creare le tabelle nella Figura 6-4 illustrato nell' Esempio 6-3.
Esempio 6-3. Il DDL SQL di Storefront
# The SQL DDL for the Storefront Application # (Programming Jakarta Struts by O'Reilly) # Chuck Cavaness # Execute the next line if you need to clear the storefront database DROP DATABASE storefront; # Creates the initial database CREATE DATABASE storefront; # Make sure you are creating the Tabellas in the storefront Tabellaspace use storefront; CREATE TABELLA CATALOG( id int NOT NULL, displaylabel varchar(50) NOT NULL, featuredcatalog char(1) NULL, description varchar(255) NULL ); ALTER TABELLA CATALOG ADD CONSTRAINT PK_CATALOG PRIMARY KEY(id); CREATE TABELLA CUSTOMER ( id int NOT NULL, firstname varchar(50) NOT NULL, lastname varchar(50) NOT NULL, email varchar(50) NOT NULL, password varchar(15) NOT NULL, description varchar(255) NULL, creditStatus char(1) NULL, accountstatus char(1) NULL, accountnumber varchar(15) NOT NULL ); ALTER TABELLA CUSTOMER ADD CONSTRAINT PK_CUSTOMER PRIMARY KEY(id); CREATE TABELLA ITEM ( id int NOT NULL, itemnumber varchar (255) NOT NULL, displaylabel varchar(50) NOT NULL, description varchar (255) NULL, baseprice decimal(9,2) NOT NULL, manufacturer varchar (255) NOT NULL, sku varchar (255) NOT NULL, upc varchar (255) NOT NULL, minsellingunits int NOT NULL, sellinguom varchar (255) NOT NULL, onhandquantity int NOT NULL, featuredesc1 varchar (255) NULL, featuredesc2 varchar (255) NULL, featuredesc3 varchar (255) NULL, smallimageurl varchar (255) NULL, largeimageurl varchar (255) NULL ) ALTER TABELLA ITEM ADD CONSTRAINT PK_ITEM PRIMARY KEY(id);
CREATE TABELLA CATALOGITEM_LNK( catalogid int NOT NULL, itemid int NOT NULL ) ALTER TABELLA CATALOGITEM_LNK ADD CONSTRAINT PK_CATALOGITEM_LNK PRIMARY KEY(catalogid, itemid); ALTER TABELLA CATALOGITEM_LNK ADD CONSTRAINT FK_CATALOGITEM_LNK_CATALOG FOREIGN KEY (catalogid) REFERENCES CATALOG(id); ALTER TABELLA CATALOGITEM_LNK ADD CONSTRAINT FK_CATALOGITEM_LNK_ITEM FOREIGN KEY (itemid) REFERENCES ITEM(id); CREATE TABELLA PURCHASEORDER ( id int NOT NULL, customerid int NOT NULL, submitdttm timestamp NOT NULL, status varchar (15) NOT NULL, totalprice decimal(9,2) NOT NULL, ) ALTER TABELLA PURCHASEORDER ADD CONSTRAINT PK_PURCHASEORDER PRIMARY KEY(id); ALTER TABELLA PURCHASEORDER ADD CONSTRAINT FK_PURCHASEORDER_CUSTOMER FOREIGN KEY (customerid) REFERENCES CUSTOMER(id); CREATE TABELLA LINEITEM ( id int NOT NULL, orderid int NOT NULL, itemid int NOT NULL, lineitemnumber int NULL, extendedprice decimal(9, 2) NOT NULL, baseprice decimal(9, 2) NOT NULL, quantity int NOT NULL ) ALTER TABELLA LINEITEM ADD CONSTRAINT PK_LINEITEM PRIMARY KEY(id); ALTER TABELLA LINEITEM ADD CONSTRAINT FK_LINEITEM_ORDER FOREIGN KEY (orderid) REFERENCES PURCHASEORDER(id); ALTER TABELLA LINEITEM ADD CONSTRAINT FK_LINEITEM_ITEM FOREIGN KEY (itemid) REFERENCES ITEM(id);
Il DDL dell'Esempio 6-3 stato testato sotto Oracle 8.1.7 e Microsoft SQL Server 2000. Se si desidera usarlo con altri database, potrebbe essere necessario modificare gli statement ALTER. Per esempio, a causa delle limitazioni di chiavi di tipo esterno con MySQL, potrebbe essere necessario eliminare interamente gli statement FOREIGN KEY. Le uniche parti assolutamente necessarie per far funzionare l'esempio sono le sezioni CREATE TABELLA e le chiavi primarie, che dovrebbero essere supportate da qualunque database. Una volta eseguito il DDL dell'Esempio 6-3, si dovranno inserire alcuni dati nelle tabelle del database per far s che l'applicazione Storefront funzioni correttamente.
Prodotto TopLink CocoBase Torque ObJectRelationalBridge FrontierSuite Castor FreeFORM Expresso JRelationalFramework VBSF JGrinder http://www.cocobase.com
URL http://otn.oracle.com/products/ias/toplink/content.html http://jakarta.apache.org/turbine/torque/index.html http://jakarta.apache.org/ojb/ http://www.objectfrontier.com http://castor.exolab.org http://www.chimu.com/projects/form/ http://www.jcorporate.com http://jrf.sourceforge.net http://www.objectmatter.com http://sourceforge.net/projects/jgrinder/
Anche se la Tabella 6-1 non costituisce una lista completa dei prodotti disponibili, presenta svariate soluzioni tra cui scegliere. Sia che si scelga un prodotto commerciale o meno, ci si dovrebbe preoccupare che l'implementazione dell'infrastruttura di mapping non "collida" con la propria applicazione. Si ricordi che in Figura 6-1 le dipendenze dovrebbero sempre percorrere i vari strati verso il basso e che non ci dovrebbe mai essere uno strato superiore con dipendenze dall'infrastruttura di persistenza. Si rivela vantaggioso persino mantenere i business object all'oscuro del modo in cui si garantisce la loro persistenza. Alcune infrastrutti re costringono a importare le loro classi e interfacce, ma questo rappresenta un problema se si avr necessit di cambiare il proprio meccanismo di persistenza. Pi oltre nel corso del capitolo si vedr come usare i pattern Business Delegate e Data Access Object (DAO) per limitare l'intrusione dell'infrastruttura di persistenza. Una tabella comparativa di questi prodotti disponibile all'URL http://www.objectrelational.com/object-relational.html. Queste informazioni non sono gratis, ma se la propria applicazione grande, potrebbe valere la pena di spendere.
Un'ultima cosa di cui avere cura consiste nel fatto che alcune infrastrutture di persistenza devono alterare il bytecode Java dei business object dopo che sono stati compilati. A seconda della reale importanza rivestita da questa operazione, potrebbero nascere dei problemi. Ci si assicuri solo di avere capito completamente come l'infrastruttura di persistenza interagisca con,ia propria applicazione prima di investire tempo e risorse nell'usarla.
Il costo stato un fattore primario. Si aveva necessit di una soluzione che si potesse utilizzare per seguire gli esempi di questo libro senza far spendere soldi. Tutte le soluzioni valutate per questo esempio hanno funzionato piuttosto bene ed erano abbastanza facili da usare ma alla fine si optato per un prodotto open source ObJectRelationalBridge (OJB).
Non si assuma che solo per il fatto che tale implementazione stata scelta Per questo libro, sia la migliore soluzione per la propria applicazione. Si prenda tempo e si valutino i prodotti seguendo i propri specifici criteri. La documentazione di OJB discreta, pur considerando che, per i progetti open source, la documentazione uno degli ultimi compiti cui si assolve. In sostanza, l'intero mapping dei business object in tabelle di un database ha luogo in un solo file XML, repository_user.xml. Il file interpretato dall'infrastruttura di mapping a runtime ed usato per eseguire SQL in un database. L'Esempio 6-4 mostra la porzione del file di mapping che effettua il mapping dei business object. Al momento della stesura di questo codice la versione pi recente di OJB era la 0.8.375. Quando questo capitolo stato aggiornato per l'ultima volta si era giunti alla versione 1.0 RC3 che era stata aggiunta alla lista dei progetti Jakarta. Il codice qui mostrato per la versione precedente. Per gli ultimi sviluppi di OJB, si veda http://jakarta.apache.org/ojb/.
Esempio 6-4. Il codice XML per il mapping della classe CustomerBO
<ClassDescriptor id="120"> <class.name>com.oreilly.struts.storefront.businessobjects.CustomerBO</clas s.name> <Tabella.name>CUSTOMER</Tabella.name> <FieldDescriptor id="1"> <field.name>id</field.name> <column.name>id</column.name> <jdbc_type>INTEGER</jdbc_type> <PrimaryKey>true</PrimaryKey> <autoincrement>true</autoincrement> </FieldDescriptor> <FieldDescriptor id="2"> <field.name>firstName</field.name> <column.name>firstname</column.name> <jdbc_type>VARCHAR</jdbc_type> </FieldDescriptor> <FieldDescriptor id="3"> <field.name>lastName</field.name> <column.name>lastname</column.name> <jdbc_type>VARCHAR</jdbc_type>
</FieldDescriptor> <FieldDescriptor id="4"> <field.name>email</field.name> <column.name>email</column.name> <jdbc_type>VARCHAR</jdbc_type> </FieldDescriptor> <FieldDescriptor id="5"> <field.name>password</field.name> <column.name>password</column.name> <jdbc_type>VARCHAR</jdbc_type> </FieldDescriptor> <FieldDescriptor id="6"> <field.name>accountStatus</field.name> <column.name>accountstatus</column.name> <jdbc_type>CHAR</jdbc_type> </FieldDescriptor> <FieldDescriptor id="7"> <field.name>creditStatus</field.name> <column.name>creditstatus</column.name> <jdbc_type>CHAR</jdbc_type> </FieldDescriptor> <CollectionDescriptor id="1"> <cdfield.name>submittedOrders</cdfield.name> <items.class>com.oreilly.struts.storefront.businessobjects.OrderBO</items .class> <inverse_fk_descriptor_ids>2</inverse_fk_descriptor_ids> </CollectionDescriptor> </ClassDescriptor>
I mapping rimanenti sono operati in un modo analogo. Una volta che sono specificati nel file XML, si deve configurare la connessione al database per permettere al driver JDBC di connettersi al database corretto. Con OJB le informazioni per la connessione si configurano tramite il file repository.xml, come da Esempio 6-5.
Esempio 6-5. Il file repository.xml contiene le informazioni per la connessione al database
<?xml version="1.0" encoding="UTF-8"?> <!-- defining entities for include-files --> <!DOCTYPE MappingRepository SYSTEM "repository.dtd" [ <!ENTITY user SYSTEM "repository_user.xml"> <!ENTITY junit SYSTEM "repository_junit.xml"> <!ENTITY internal SYSTEM "repository_internal.xml"> ]> <MappingRepository> <JdbcConnectionDescriptor id="default"> <dbms.name>MsSQLServer2000</dbms.name> <jdbc.level>1.0</jdbc.level> <driver.name>com.microsoft.jdbc.sqlserver.SQLServerDriver</driver.name> <url.protocol>jdbc</url.protocol> <url.subprotocol>microsoft:sqlserver</url.subprotocol> <url.dbalias> //localhost:1433;DatabaseName=storefront;user=sa;SelectMethod=cursor </url.dbalias> <user.name>sa</user.name> <user.passwd></user.passwd> </JdbcConnectionDescriptor> <!-- include user-defined mappings here --> &user; <!-- include ojb internal mappings here --> &internal;
</MappingRepository>
Si devono ovviamente configurare i parametri presenti in questo file per il proprio specifico ambiente. Tutto ci serve per configurare l'infrastruttura di persistenza per la propria applicazione sta tutto in questo file. Per inizializzare questa infrastruttura all'interno della propria applicazione si devono chiamare tre metodi, come viene mostrato pi oltre in questa sezione. OJB offre due diverse API che possono essere utilizzate: il Persistence-Broker e l'implementazione di ODMG. Si pu leggere la documentazione per una comprensione approfondita delle differenze, ma in questa sede sufficiente dire che ODMG molto pi potente e possiede un proprio Object Query Language (OQL). Tuttavia alla funzionalit extra si accompagna un aumento della complessit. L'applicazione Storefront utilizzer l'API ODMG perch possiede caratteristiche migliori. L'ODMG permette inoltre al framework di poter essere utilizzato in modalit standalone oppure client/server. Quest'ultima modalit utilissima qualora vi siano numerosi server nello stesso ambiente. Si scelto di usarla per in modalit standalone perch Storefront non necessita di scalabilit. L'infrastruttura di persistenza gira perci all'interno della stessa JVM come pure l'applicazione Storefront.
Non c' spazio a sufficienza in questo capitolo per una spiegazione pi accurata del framework degli OJB. Per informazioni maggiormente dettagliate si consulti la documentazione disponibile su http://jakarta.apache.org/ojb/. Non si dimentichi che vi sar la necessit di avere un database supportato e un driver JDBC all'interno del classpath della propria web application.
L'oggetto client della Figura 6-5 rappresenta le classi Action di Struts. Queste otterranno un riferimento per un'interfaccia service cui ci si riferisce nel diagramma come Business Delegate Interface. L'interfaccia business di Storefront illustrata nell'Esempio 6-6. Esempio 6-6. The Storefront business interface
package com.oreilly.struts.storefront.service; import java.util.List; import com.oreilly.struts.storefront.catalog.view.ItemDetailView; import com.oreilly.struts.storefront.catalog.view.ItemSummaryView; import com.oreilly.struts.storefront.framework.exceptions.DatastoreException; import com.oreilly.struts.storefront.framework.security.IAuthentication; /** * The business interface for the Storefront application. It defines all * of the methods that a client may call on the Storefront application. * This interface extends the IAuthentication interface to provide a * single cohesive interface for the Storefront application. */ public interface IStorefrontService extends IAuthentication { public List getFeaturedItems( ) throws DatastoreException;
IStorefrontService estende la classe IAuthentication per incapsulare i metodi relativi alla sicurezza. La classe IAuthentication dell'Esempio 6-7, contiene solo due metodi per questosemplice esempio.
package com.oreilly.struts.storefront.framework.security; import com.oreilly.struts.storefront.customer.view.UserView; import com.oreilly.struts.storefront.framework.exceptions.InvalidLoginException; import com.oreilly.struts.storefront.framework.exceptions.ExpiredPasswordException; import com.oreilly.struts.storefront.framework.exceptions.AccountLockedException; import com.oreilly.struts.storefront.framework.exceptions.DatastoreException; /** * Defines the security methods for the system. */ public interface IAuthentication { /** * Log the user out of the system. */ public void logout(String email); /** * Authenticate the user's credentials and either return a UserView for the * user or throw one of the security exceptions. */ public UserView authenticate(String email, String password) throws InvalidLoginException, ExpiredPasswordException, AccountLockedException, DatastoreException; }
Nell'Esempio 6-8 si pu analizzare, un'implementazione per l'interfaccia service IStorefrontService. L'implementazione potrebbe essere sostituita senza problemi da altre purch le nuove implementazioni contengano l'interfaccia IStorefrontService. Nessun client avrebbe alcun problema perch la programmazione per loro fatta a fronte dell'interfaccia e non dell'implementazione. Si potr vedere un esempio di sostituzione dell'implementazione dell'interfaccia IStorefrontService nel corso del Capitolo 13, quando sostituiremo uno strato EJB nell'applicazione Storefront.
package com.oreilly.struts.storefront.service; import java.sql.Timestamp; import java.util.List; import java.util.ArrayList; import com.oreilly.struts.storefront.catalog.view.ItemDetailView; import com.oreilly.struts.storefront.catalog.view.ItemSummaryView; import com.oreilly.struts.storefront.framework.security.IAuthentication; import com.oreilly.struts.storefront.customer.view.UserView; import com.oreilly.struts.storefront.businessobjects.*; // Import the exceptions used import com.oreilly.struts.storefront.framework.exceptions.DatastoreException; import com.oreilly.struts.storefront.framework.exceptions.InvalidLoginException; import com.oreilly.struts.storefront.framework.exceptions.ExpiredPasswordException;
import com.oreilly.struts.storefront.framework.exceptions.AccountLockedException; // Import the implementation-specific packages import org.odmg.*; import ojb.odmg.*; public class StorefrontServiceImpl implements IStorefrontService{ // Implementation-specific references Implementation odmg = null; Database db = null; /** * Create the service, which includes initializing the persistence * framework. */ public StorefrontServiceImpl( ) throws DatastoreException { super( ); init( ); } /** * Return a list of items that are featured. */ public List getFeaturedItems( ) throws DatastoreException { // Start a transaction Transaction tx = odmg.newTransaction( ); tx.begin( ); List results = null; try{ OQLQuery query = odmg.newOQLQuery( ); // Set the OQL select statement query.create( "select featuredItems from " + ItemBO.class.getName( ) ); results = (List)query.execute( ); tx.commit( ); }catch( Exception ex ){ // Roll back the transaction tx.abort( ); ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } int size = results.size( ); List items = new ArrayList( ); for( int i = 0; i < size; i++ ){ ItemBO itemBO = (ItemBO)results.get(i); ItemSummaryView newView = new ItemSummaryView( ); newView.setId( itemBO.getId().toString( ) ); newView.setName( itemBO.getDisplayLabel( ) ); newView.setUnitPrice( itemBO.getBasePrice( ) ); newView.setSmallImageURL( itemBO.getSmallImageURL( ) ); newView.setProductFeature( itemBO.getFeature1( ) ); items.add( newView ); } return items; } /** * Return a detailed view of an item based on the itemId argument. */ public ItemDetailView getItemDetailView( String itemId ) throws DatastoreException{ // Start a transaction Transaction tx = odmg.newTransaction( ); tx.begin( ); List results = null; try{
); );
// Set the OQL select statement String queryStr = "select item from " + ItemBO.class.getName( queryStr += " where id = $1"; query.create(queryStr); query.bind(itemId); // Execute the transaction results = (List)query.execute( ); tx.commit( ); }catch( Exception ex ){ // Roll back the transaction tx.abort( ); ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } if (results.isEmpty( ) ){ throw DatastoreException.objectNotFound( } ItemBO itemBO = (ItemBO)results.get(0); // Build a ValueObject for the Item ItemDetailView view = new ItemDetailView( ); view.setId( itemBO.getId().toString( ) ); view.setDescription( itemBO.getDescription( ) ); view.setLargeImageURL( itemBO.getLargeImageURL( ) ); view.setName( itemBO.getDisplayLabel( ) ); view.setProductFeature( itemBO.getFeature1( ) ); view.setUnitPrice( itemBO.getBasePrice( ) ); view.setTimeCreated( new Timestamp(System.currentTimeMillis( view.setModelNumber( itemBO.getModelNumber( ) ); return view; } );
) ));
/** * Authenticate the user's credentials and either return a UserView for the * user or throw one of the security exceptions. */ public UserView authenticate(String email, String password) throws InvalidLoginException,ExpiredPasswordException,AccountLockedException, DatastoreException { // Start a transaction Transaction tx = odmg.newTransaction( tx.begin( ); );
// Query the database for a user that matches the credentials List results = null; try{ OQLQuery query = odmg.newOQLQuery( ); // Set the OQL select statement String queryStr = "select customer from " + CustomerBO.class.getName( ); queryStr += " where email = $1 and password = $2"; query.create(queryStr); // Bind the input parameters query.bind( email ); query.bind( password ); // Retrieve the results and commit the transaction results = (List)query.execute( ); tx.commit( ); }catch( Exception ex ){
// Roll back the transaction tx.abort( ); ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } // If no results were found, must be an invalid login attempt if ( results.isEmpty( ) ){ throw new InvalidLoginException( ); } // Should only be a single customer that matches the parameters CustomerBO customer = (CustomerBO)results.get(0); // Make sure the account is not locked String accountStatusCode = customer.getAccountStatus( ); if ( accountStatusCode != null && accountStatusCode.equals( "L" ) ){ throw new AccountLockedException( ); } // Populate the value object from the Customer business object UserView userView = new UserView( ); userView.setId( customer.getId().toString( ) ); userView.setFirstName( customer.getFirstName( ) ); userView.setLastName( customer.getLastName( ) ); userView.setEmailAddress( customer.getEmail( ) ); userView.setCreditStatus( customer.getCreditStatus( ) ); return userView; } /** * Log the user out of the system. */ public void logout(String email){ // Do nothing with it right now, but might want to log it for auditing reasons } /** * Opens the database and prepares it for transactions */ private void init( ) throws DatastoreException { // Get odmg facade instance odmg = OJB.getInstance( ); db = odmg.newDatabase( ); // Open database try{ db.open("repository.xml", Database.OPEN_READ_WRITE); }catch( Exception ex ){ throw DatastoreException.datastoreError(ex); } } }
L'implementazione del servizio presenta tutti i metodi richiesti dell'interfaccia IStorefrontService. Dal momento che IStorefrontService estende l'interfaccia IAuthentication, anche la classe StorefrontServiceImpl deve implementare i metodi per la sicurezza. Si noti che di nuovo l'implementazione non ha alcuna consapevolezza del framework Struts o dei web container in generale. Questo permette la riusabilit per numerosi tipi di applicazioni differenti, cosa che all'inizio del capitolo ci eravamo posti come obiettivo. Si detto prima che si dovranno invocare alcuni metodi del framework OJB cos che l'XML che provvede alla mappatura possa essere letto e le connessioni al database possano essere effettuate con prontezza. Questa inizializzazione mostrata nel metodo init() nell'Esempio 6-8. Quando chiamato il costruttore di questa implementazione, il file XML caricato in memoria e letto. Il framework di persistenza pronto alle chiamate in seguito a un completamento corretto delle operazioni del costruttore. Il costruttore deve essere invocato dal client nel caso di Storefront si utilizzer una classe factory, che sar anche un PlugIn per Struts, per la determinazione del servizio Storefront da inizializzare. La factory illustrata nell' Esempio 6-9.
package com.oreilly.struts.storefront.service; import import import import import import javax.servlet.ServletContext; javax.servlet.ServletException; org.apache.struts.action.PlugIn; org.apache.struts.action.ActionServlet; org.apache.struts.config.ApplicationConfig; com.oreilly.struts.storefront.framework.util.IConstants;
/** * A factory for creating Storefront service implementations. The specific * service to instantiate is determined from the initialization parameter * of the ServiceContext. Otherwise, a default implementation is used. * */ public class StorefrontServiceFactory implements IStorefrontServiceFactory,PlugIn{ // Hold on to the servlet for the destroy method private ActionServlet servlet = null; // The default is to use the debug implementation String serviceClassname = "com.oreilly.struts.storefront.service.StorefrontDebugServiceImpl"; public IStorefrontService createService( ) throws ClassNotFoundException, IllegalAccessException, InstantiationException { String className = servlet.getInitParameter( IConstants.SERVICE_CLASS_KEY ); if (className != null ){ serviceClassname = className; } return (IStorefrontService)Class.forName(className).newInstance( } public void init(ActionServlet servlet, ApplicationConfig config) throws ServletException{ // Store the servlet for later this.servlet = servlet; /* Store the factory for the application. Any Storefront service factory * must either store itself in the ServletContext at this key or extend * this class and don't override this method. The Storefront application * assumes that a factory class that implements the IStorefrontServiceFactory * is stored at the proper key in the ServletContext. */ servlet.getServletContext( ).setAttribute( IConstants.SERVICE_FACTORY_KEY, this ); } public void destroy( ){ // Do nothing for now } }
La classe StorefrontServiceFactory dell'Esempio 6-9 legge un parametro di inizializzazione dal file web.xml, che riporta il nome della classe che implementa IStorefrontService da istanziare. Se non possiede in initparam per questo valore, viene creata una classe di implementazione predefinita (in questo caso, l'implementazione di debug). Dal momento che la classe factory implementa l'interfaccia PlugIn, sar istanziata all'avvio e sar invocato il metodo init(). Il metodo init() conserva un'istanza della factoryall'interno dello scope di applicazione, dove in seguito pu essere recuperato. Per creare un'istanza del servizio Storefront, un client deve recuperare la factory dal
);
package com.oreilly.struts.storefront.security; import import import import import import import import import java.util.Locale; javax.servlet.http.*; org.apache.struts.action.*; com.oreilly.struts.storefront.customer.view.UserView; com.oreilly.struts.storefront.framework.exceptions.BaseException; com.oreilly.struts.storefront.framework.UserContainer; com.oreilly.struts.storefront.framework.StorefrontBaseAction; com.oreilly.struts.storefront.framework.util.IConstants; com.oreilly.struts.storefront.service.IStorefrontService;
/** * Implements the logic to authenticate a user for the Storefront application. */ public class LoginAction extends StorefrontBaseAction{ /** * Called by the controller when a user attempts to log in to the * Storefront application. */ public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{ // Get the user's login name and password. They should have already // been validated by the ActionForm. String email = ((LoginForm)form).getEmail( ); String password = ((LoginForm)form).getPassword( ); // Log in through the security service IStorefrontService serviceImpl = getStorefrontService( ); UserView userView = serviceImpl.authenticate(email, password); UserContainer existingContainer = null; HttpSession session = request.getSession(false); if ( session != null ){ existingContainer = getUserContainer(request); session.invalidate( ); }else{ existingContainer = new UserContainer( ); } // Create a new session for the user session = request.getSession(true); existingContainer.setUserView(userView); session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer); return mapping.findForward(IConstants.SUCCESS_KEY); } }
La prima linea evidenziata invoca il metodo getStorefrontService(). Questo metodo collocato nella superclasse chiamata StorefrontBaseAction perch ogni classe Action dovr avere la possibilit di invocarlo. L'implementazione del metodo getStorefrontService() ritira la factory e invoca a sua volta il metodo createService(). Nell'Esempio 6-11 mostrata la classe StorefrontBaseAction, che include il metodo getStorefrontService().
package com.oreilly.struts.storefront.framework; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Iterator; import javax.servlet.http.*; import org.apache.struts.action.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.oreilly.struts.storefront.framework.util.IConstants; import com.oreilly.struts.storefront.framework.exceptions.*; import com.oreilly.struts.storefront.service.IStorefrontService; import com.oreilly.struts.storefront.service.IStorefrontServiceFactory; /** * An abstract Action class that all Storefront action classes should * extend. */ abstract public class StorefrontBaseAction extends Action{ Log log = LogFactory.getLog( this.getClass( ) ); protected IStorefrontService getStorefrontService( ){ IStorefrontServiceFactory factory = (IStorefrontServiceFactory) getApplicationObject( IConstants.SERVICE_FACTORY_KEY ); IStorefrontService service = null; try{ service = factory.createService( ); }catch( Exception ex ){ log.error( "Problem creating the Storefront Service", ex ); } return service; } /** * Retrieve a session object based on the request and the attribute name. */ protected Object getSessionObject(HttpServletRequest req, String attrName) { Object sessionObj = null; // Don't create a session if one isn't already present HttpSession session = req.getSession(true); sessionObj = session.getAttribute(attrName); return sessionObj; } /** * Return the instance of the ApplicationContainer object. */ protected ApplicationContainer getApplicationContainer( ) { return (ApplicationContainer) getApplicationObject(IConstants.APPLICATION_CONTAINER_KEY); } /** * Retrieve the UserContainer for the user tier to the request. */ protected UserContainer getUserContainer(HttpServletRequest request) { UserContainer userContainer = (UserContainer)getSessionObject(request, IConstants.USER_CONTAINER_KEY); // Create a UserContainer for the user if it doesn't exist already
if(userContainer == null) { userContainer = new UserContainer( ); userContainer.setLocale(request.getLocale( )); HttpSession session = request.getSession( ); session.setAttribute(IConstants.USER_CONTAINER_KEY, userContainer); } return userContainer; } /** * Retrieve an object from the application scope by its name. This is * a convenience method. */ protected Object getApplicationObject(String attrName) { return servlet.getServletContext( ).getAttribute(attrName); } }
Il metodo getApplicationObject() soltanto un metodo utile alle classi Action di Storefront;invoca il metodo getAttribute() sull'oggetto ServletContext. Infine la seconda linea evidenziata nell'Esempio 6-10 invoca un metodo di servizio sull'implementazione. Viene invocato un metodo authenticate() ed restituito un oggetto valore chiamato UserView per Action:
6.5.8 Conclusioni
n questo capitolo si sono trattati molti argomenti importanti e potrebbe addirittura disorientare se si nuovi ai meccanismi della persistenza. Alcuni dei prossimi capitoli partiranno dal presupposto che questi argomenti siano ormai acquisiti, per cui importante aver compreso bene quanto detto fin qui prima di procedere oltre.
CAPITOLO 7
In questo capitolo saranno introdotti i componenti che costituiscono lo strato view di Struts. L'infrastruttura utilizza i componenti di view per la presentazione del contenuto dinamico al client. Basati soprattutto sulle Java Server Pages, i componenti offrono un supporto per l'internazionalizzazione, l'accettazione dell'input dell'utente, la validazione e la gestione degli errori, rendendo possibile agli sviluppatori di concentrarsi sui requisiti alla base dell'applicazione. Questo capitolo conclude la discussione sul modo in cui Struts implementa le lince guida del Model-View-Controller (MVC). Verr anche trattata brevemente la specifica delle future JavaServer Faces. Anche se queste specifiche sono in una fase iniziale di scrittura, probabile che in futuro abbiano un certo impatto sul framework Struts.
Figura 7-1. La pagina principale di Storefront rappresenta una prospettiva del modello.
Quando un utente seleziona uno degli oggetti in vendita vengono mostrati anche i dettagli. L'utente sta guardando ancora lo stesso modello di business, ma la view diversa. La Figura 7-1 illustra questa vista alternativa del modello.
Per questa pagina necessaria una visione maggiormente dettagliata del modello di business: potrebbero infatti esserci pagine JSP differenti, immagini, contenuti multimediali e altri componenti. Queste due diverse prospettive sono di fatto due punti di vista diversi dello stesso modello. Dal momento che i business object non possiedono un modo naturale di rappresentazione visiva, sta ai componenti di rappresentazione (view) il compito di presentare le informazioni del modello di dominio ai client. Questa presentazione potrebbe essere nella forma di XML e XSLT, messaggi SOAP destinati a un servizio web client oppure, come in Storefront, HTML interpretato da un browser. Un tipo diverso di client, come un apparato wireless, potrebbe considerare un insieme completamente diverso di view e continuare a usare lo stesso modello. Il modello utilizzato per rappresentare lo stato di un'applicazione, mentre le view servono a presentarlo al client, sia totalmente che parzialmente. Sebbene l'applicazione Storefront sia un'applicazione B2C, possibile avere delle applicazioni B2B che usano lo stesso modello per rendere disponibili ai partner le funzionalit che riguardano gli ordini degli articoli. Finch si mantiene una adeguata separazione tra modello e livello di presentazione si pu creare qualsiasi numero di view per tutti i client che si desidera sulla base dello stesso modello di dominio.
Immagini (.gif, .jpg, etc.) Audio (.wav, .mp3, etc.) Video (.avi, .mpg, etc.) Le immagini sono con buona probabilit i file multimediali maggiormente usati. Per applicazioni B2B sono invece prevalenti file audio e anche video (ad esempio per illustrare disegni CAD e specifiche). Struts supporta l'utilizzo di file multimediali all'interno di un'applicazione. Questo supporto si ottiene soprattutto attraverso l'impiego di tag personalizzati ma possibile usare anche l'HTML per questo tipo di risorse. Ci sono alcuni problemi relativi alle immagini con i tag personalizzati. Le differenze tra patti relativi e assoluti potrebbero rendere impossibile la visualizzazione di alcune immagini. Il miglior approccio consiste nel deci ere se usare path relativi o assoluti ed essere poi il pi d possibile coerenti con la propria decisione.
Uno dei trabocchetti in cui gli sviluppatori cadono pi frequentemente quando impiegano i JavaBeans consiste nell'impiego di un valore tipo di ritorno diverso dal tipo del parametro. Se si crea in un JavaBean un metodo che passa come argomento una String:
Se il tipo restituito noi uguale al tipo del parametro, Struts potrebbe non riconoscerlo come bean, e con buone probabilit si otterrebbe un errore del tipo: "No getter method could be found" oppure "No setter method could be found" per il nome della propriet.
);
i pulsanti, le checkbox e altro. Quando si costruiscono questi tipi di pagine, si devono annidare i componenti dell'input all'interno di elementi di un form HTML. L'Esempio 7-1 illustra una pagina di sign-in molto semplice, simile a quella utilizzata nell'applicazione Storefront.
Esempio 7 1. Una semplice pagina di sign-in
<html> <head> <title>Esempio 7-1. OReilly Struts Book</title> <link rel="stylesheet" href="stylesheets/main.css" type="text/css"> </head> <body> <form method="post" action="/action/signin"> <!-- The Tabella layout for the email and password fields --> <Tabella BORDER="0" cellspacing="0" cellpadding="0"> <tr> <td>Email: </td> <td> </td> <td> <input type="text" name="email" size="20" maxlength="20"/> </td> </tr> <tr> <td>Password:</td> <td> </td> <td class="alignformslist"> <input type="text" name="password" size="20" maxlength="25"/> </td> </tr> <!-- The Tabella layout for the signin button --> <Tabella width="250" border="0"> <tr> <td> <input type="submit" name="Submit" value="Signin" class="Buttons"> </td> </tr> </Tabella> </form> </body> </html>
Quando l'utente preme il pulsante Signin nel form HTML dell'Esempio 7-1, i valori all'interno del campo sono trasmessi con la richiesta HTTP. L'applicazione server pu recuperare i dati inseriti, validarli in seguito trasmetterli a un altro componente dell'applicazione dove avviene l'autenticazione vera e propria. Se i dati in ingresso non vengono validati, l'applicazione li dovrebbe trasmettere indietro insieme a un messaggio d'errore che spieghi il fallimento del login. Sarebbe un compito arduo gestire manualmente tutte queste funzionalit, dal recupero dei valori all'esecuzione della validazione, alla trasmissione di messaggi d'errore in caso di fallimento. Questo tipo di azioni sono compiute in molte parti di una web application e sarebbe bello che a prendersene carico fosse il framework, che poi si occupi anche della possibilit di riutilizzarle in altre applicazioni. Per fortuna Struts offre questa funzionalit e gestir questi compiti per conto dell'applicazione. Il framework Struts si basa sulla classe org.apache.struts.action.ActionForm come componente chiave per gestire questi compiti. La classe ActionForm impiegata per catturare i dati in ingresso da un form HTML e trasferirli alla classe Action. Dal momento che gli utenti spesso inseriscono dati non validi, le web application necessitano di un modo per conservare i dati temporaneamente per poterli poi inviare di nuovo quando si verifica un errore. In questo senso la classe ActionForm agisce da buffer per mantenere lo stato dei dati inviati dall'utente mentre sono validati. La classe ActionForm opera anche come "firewall" per la propria applicazione dal momento che contribuisce a tenere dati in ingresso sospetti o non validi fuori dallo strato applicativo fino alla loro effettiva validazione ad opera delle regole definite. In ultimo, quando i dati sono restituiti dal livello applicativo, pu essere riempita una particolare ActionForm che pu essere impiegata da una pagina JSP per effettuare la visualizzazione dei campi di input di un modulo HTML.
Questo permette ai form HTML di utilizzare una coerenza maggiore, dal momento che i dati sono sempre estratti da ActionForm e non da JavaBeans differenti. Quando i dati inseriti dall'utente passano la validazione dell'input, ActionForm trasmesso nel metodo execute() della classe Action. Da quel punto i dati possono poi essere recuperati a partire da ActionForm ed essere trasmessi a livello applicativo. Dal momento che ActionForm importa i propri package dall'API Servlet, non si dovrebbe passare l'ActionForm allo strato applicativo, dal momento che si assocerebbero i metodi dell'applicazione allo strato dell'applicazione stessa e la riusabilit dei componenti dello strato applicativo sarebbe pi difficoltosa. I dati all'interno dell'ActionForm dovrebbero essere trasferiti a un oggetto all'interno del modello di dominio. Un approccio comunemente impiegato consiste nel creare un DTO e nel popolarlo con i dati dell'ActionForm. Non si dovr dichiarare una ActionForm per ogni form HTML presente nella propria applicazione. La stessa ActionForm pu essere associata con uno o pi mapping di azioni. Questo significa che possono essere distribuite in pi form HTML. Per esempio, se si avesse un interfaccia di tipo wizard dove alcuni dati sono inseriti e copiati su pi pagine, si potrebbe impiegare una sola ActionForm per raccogliere tutti questi dati.
La Figura 7-4 mostra soltanto i passi attinenti a una ActionForm, non tutti quelli cui una request va incontro durante la sua l'elaborazione. Si noti che, qualora una ActionForm rilevi uno o pi errori di validazione, compie un forward verso la risorsa identificata nell'attributo input. I dati inviati insieme alla richiesta sono lasciati in ActionForm perch questa possa poi usarli per ripopolare i campi HTML.
package com.oreilly.struts.storefront.security; import javax.servlet.http.HttpServletRequest; import org.apache.struts.action.*; /** * Form bean for the user signin page. */ public class LoginForm extends ActionForm { private String email = null; private String password = null; public void setEmail(String email) { this.email = email; } public String getEmail( return (this.email); } ) {
) {
public void setPassword(String password) { this.password = password; } /** * Validate the properties that have been sent from the HTTP request, * and return an ActionErrors object that encapsulates any * validation errors that have been found. If no errors are found, return * an empty ActionErrors object. */ public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { ActionErrors errors = new ActionErrors( ); if( getEmail() == null || getEmail().length( ) < 1 ) { errors.add("email", new ActionError("security.error.email.required")); } if( getPassword() == null || getPassword().length( ) < 1 ){ errors.add("password", new ActionError("security.error.password.required")); } return errors; } public void reset(ActionMapping mapping, HttpServletRequest request) { /** Because this ActionForm should be request-scoped, do nothing here. * The fields will be reset when a new instance is created. We could * have just not overriden the parent reset( ) method, but we did so * to provide an Esempio of the reset( ) method signature. */ } }
Quando si invia il form, viene creata;un'istanza di LoginForm che popolata dai parametri di request. Il framework compie questa azione comparando ogni parametro di richiesta con il corrispondente nome della propriet nella classe ActionForm. ActionForm viene riempita da parametri di request, non da attributi di request. Se si compie un forward da un'azione a un'altra non si pu poi aggiungere un attributo request ed aspettarsi che ActionForm sia da questa popolata. I parametri di request e gli attributi di request sono due risorse separate.
<action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action> Quando invocata l'azione signin, il framework completa un'istanza di una LoginForm per mezzo dei valori contenuti nella richiesta. Dal momento che l'attributo validate ha come valore true, sar invocato il metodo validate() in LoginForm. Anche se l'attributo validate impostato come false, ActionForm sar comunque popolata dalla
richiesta.
Il metodo validate() nella classe ActionForm restituisce semplicemente una stringavuota. Se si desidera validare i dati sottoposti con la richiesta si dovr evitare il metodo validate() nelle proprie sottoclassi di ActionForm, cos come riportato dall'Esempio 72. Il metodo validate() potrebbe restituire un oggetto ActionErrors, a seconda se siano stati rilevati errori di validazione. Potrebbe anche restituire un valore vuoto se non ci sono errori. Il framework controller la sussistenza di entrambe le condizioni, sia valori vuoti che oggetti vuoti del tipo ActionErrors. Questo evita allo sviluppatore di creare un'istanza di ActionErrors quando non ci sono errori. La classe ActionError e la sua classe padre, ActionMessage, saranno esaminate in dettaglio pi oltre in questo capitolo.
type="com.oreilly.struts.storefront.security.LoginAction" validate="true"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action> Per maggiori informazioni circa gli attributi dell'elemento action, si veda "Il DTD di configurazione di Struts" nel
Capitolo 4.
public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{ // Get the user's login name and password. They should already have // been validated by the ActionForm. String email = ((LoginForm)form).getEmail( ); String password = ((LoginForm)form).getPassword( ); // Log in through the security service. IStorefrontService serviceImpl = getStorefrontService( ); UserView userView = serviceImpl.authenticate(email, password); UserContainer existingContainer = null; HttpSession session = request.getSession(false); if ( session != null ){ existingContainer = getUserContainer(request); session.invalidate( ); }else{ existingContainer = new UserContainer( ); } // Create a new session for the user. session = request.getSession(true); existingContainer.setUserView(userView); session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer); return mapping.findForward(IConstants.SUCCESS_KEY); }
Non obbligatorio impiegare una ActionForm per recuperare i dati da un HTML. Anche se se form non si dichiara unaActionForm per un form i dati possono essere sempre ricavati a partire dalla request. Tuttavia l'applicazione dovr essere in grado di gestire il processo di validazione eerrori gli dalla classe Action.
All request parameters that are sent by the browser are strings. This is true regardless of the type that the value will eventually map to in Java. For Esempio, dates, times, Booleans, and other values all are strings when they are pulled out of the request, and they will be converted into strings when they are written back out to the HTML page. Therefore, all of the ActionForm properties where the input may be invalid should be of type String, so that the data can be displayed back to the user in its original form when an error occurs. For Esempio, say a user types in "12Z" for a property expecting an Integer. There's no way to store "12Z" into an int or Integer property, but you can store it into a String until it can be validated. This value can be used to render the input field with the value, so the user can see his mistake. Even the most inexperienced users have come to expect and look for this functionality.
Un'istanza di ActionErrors pu essere istanziata nel metodo validate() e pu essere popolata aggiungendovi istanze della classe ActionError. Nel LoginForm dell'Esempio 7-2 avviene proprio questo e si riprende qui lo stesso procedimento per comodit:
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request){ ActionErrors errors = new ActionErrors( ); if( getEmail() == null || getEmail().length( ) < 1 ){ errors.add("email", new ActionError("security.error.email.required"));
} if( getPassword() == null || getPassword().length( errors.add("password", new ActionError("security.error.password.required")); } return errors; } ) < 1 ){
Il metodo validate() in questa porzione di codice compie i controlli necessari sui campi email e password. Se la stringa risultante vuota, si aggiungono degli oggetti ActionError all'istanza ActionError. La classe ActionError contiene parecchi costruttori utili: alcuni di questi sono elencati di seguito.
key); key, Object value0); key, Object value0, Object value1); key, Object[] values);
L'argomento chiave un valore String che corrisponde a una chiave dei resource bundle di un'applicazione. Il tag personalizzato ErrorsTag impiega questo valore per ricercare il messaggio da mostrare all'utente. Gli argomenti restanti sono usati come valori di sostituzione dei parametri per il messaggio. Se si ha un bundle di messaggi cos definito:
org.apache.struts.action.ActionMessage funziona nello stesso modo della classe ActionError: di fatto stata aggiunta in qualit di superclasse per la classe ActionError.
Il motivo principale per cui la classe ActionMessage stata aggiunta al framework consiste nel fatto che molti programmatori la utilizzano come mezzo per veicolare messaggi generici o di avvertimento, mentre, come implica il nome stesso, dovrebbe essere ristretta al solo ambito dei messaggi di errore. Per questo era auspicabile l'introduzione di una nuova classe appositamente pensata per utilizzi generici. ActionMessage viene impiegata esattamente allo stesso modo della classe ActionError, fatta eccezione per il fatto che rappresenta un messaggio corrispondente a un evento meno grave di un errore da inviare a un utente. Le istanze di questa classe vengono create allo stesso modo ma sono aggiunte a un oggetto ActionMessages anzich ad un oggetto ActionErrors. Dal momento che ActionError solo un messaggio particolare, estende la classe ActionMessage. Nella Figura 7-5 illustrata la relazione tra queste due classi.
L'Esempio 7-4 illustra come inserire ActionError nelle richieste impiegando la classe LoginAction.
Esempio 7 4. Creazione di ActionErrors a partire dal metodo execute()
public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{
/** * Get the user's email and password, which should already have been * validated by the ActionForm. */ String email = ((LoginForm)form).getEmail( ); String password = ((LoginForm)form).getPassword( ); // Log in through the security service IStorefrontService serviceImpl = getStorefrontService( );
UserView userView = null; try{ userView = serviceImpl.authenticate(email, password); }catch( InvalidLoginException ex ){ ActionErrors errors = new ActionErrors( ); ActionError newError = new ActionError( "security.login.failed" ); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors ); // Return back to the previous state return mapping.findForward( mapping.getInput( ) ); } // Authenticate was successful UserContainer existingContainer = null; HttpSession session = request.getSession(false); if ( session != null ){ existingContainer = getUserContainer(request); session.invalidate( ); }else{ existingContainer = new UserContainer( ); } // Create a new session for the user session = request.getSession(true); existingContainer.setUserView(userView); session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer); return mapping.findForward(IConstants.SUCCESS_KEY); } }
Nell'Esempio 7-4, quando generata un'eccezione InvalidLoginException dal metodo authenticate(), l'eccezione viene intercettata ed creato un ActionError. Il metodo saveErrors() esiste nelle classi base di Struts e conserva l'oggetto ActionErrors nella richiesta. Esiste anche un corrispondente metodo saveMessages() per usi analoghi. Una volta che una ActionError o una ActionMessage sono immagazzinate nelle request e il controllo passato a una pagina JSP, pu essere impiegato uno dei lag personalizzati del framework per la visualizzazione del messaggio per l'utente. Questi tag sono introdotti nel capitolo successivo.
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { ActionErrors errors = new ActionErrors( ); String orderQtyStr = getQuantity( );
if( orderQtyStr == null || orderQtyStr.length( ) < 1 ){ errors.add( ActionErrors.GLOBAL_ERROR, new ActionError( "order.quantity.required" )); } // Validate that the qty entered was in fact a number try{ // Integer.parse was not used because it's not really I18N-safe java.text.Format format = java.text.NumberFormat.getNumberInstance( Number orderQty = (Number)format.parseObject( orderQtyStr ); }catch( Exception ex ){ // The quantity entered by the user was not a valid qty errors.add( ActionErrors.GLOBAL_ERROR, new ActionError( "order.quantity.invalid" )); } return errors; }
Come si pu arguire, le web application spesso necessitano di un controllo sui valori richiesti o di accertare che i dati inseriti siano aderenti a un certo formato oppure siano di un certo tipo. Dal momento che tutti i dati recuperati da una richiesta sono del tipo String, ci si deve assicurare che i dati non vadano a introdurre errori nei componenti dell'elaborazione. Anche se si pu effettuare la validazione tramite programmazione, l'infrastruttura Struts offre un'alternativa esterna ad ActionForm e al metodo validate(). Di fatto la maggior parte delle regole di validazione sono gi delinite, per cui non si deve scrivere alcun codice per queste ultime. Tutto quello che si deve fare configurare in maniera dichiarativa le regole di cui si necessita, in un file XML aggiuntivo. Il Validator di Struts sar affrontato nel Capitolo 11.
);
<form-beans> <form-bean name="loginForm" type="org.apache.struts.action.DynaActionForm"> <!-- Specify the dynamic properties of the form --> <form-property name="email" type="java.lang.String "/> <form-property name="password" type="java.lang.String "/> <!-- You can also set the initial value of a property --> <form-property initial="false" name="rememberMe" type="java.lang.Boolean "/> </form-bean> <form-beans>
Sono le propriet dichiarative a rendere dinamica ActionForm. Durante l'esecuzione il framework crea un'istanza della classe DynaActionForm che rende possibile impostare e recuperare i valori configurati delle propriet; non c' bisogno di effettuare cambiamenti nel codice sorgente, cosa che aumenta potenza e flessibilit. Come delineato nel Capitolo 4 l'elemento form-property permette anche di specificare il valore iniziale per ciascuna propriet. Il framework imposta la propriet a quel valore all'avvio dell'applicazione. Il valore iniziale usato anche quando invocato il metodo reset()per riportare i valori allo stato iniziale. Il metodo reset() diversamente dalla classe ActionForm in cui, in maniera predefinita, reset() non fa nulla in DynaActionForm riporta tutti i valori allo stato originale. Se non si include l'attributo initial per una propriet, a questo verr assegnato un valore predefinito basato sulle convenzioni di programmazione di Java: i numeri verranno reimpostati a zero (0) e le propriet del tipo Object a null.
L'attributo type si aspetta un nome completo di classe Java. Perci si dovranno sempre usare i wrapper Java dei tipi primitivi (ad esempio, java.lang.Boolean per un tipo di propriet booleana, java.lang.Integer per una propriet int e cos via). Tuttavia, come gi spiegato in questo capitolo, si dovrebbero usare solo propriet di tipo String anche con DynaActionForm.
CAPITOLO 8
Il supporto per le custom action (dette anche tag personalizzati) nelle JSP stato introdotto a partire dalle specifiche JSP 1.1. Questa funzionalit permette agli sviluppatori di estendere i tag disponibili ben oltre le possibilit delle sole JSP. I tag personalizzati sono raggruppati in librerie di tag riutilizzabili. Struts sfrutta la caratteristica delle librerie di tag JSP con l'inclusione di svariate categorie di tag che contribuiscono a rendere lo strato di presentazione pi facilmente gestibile e riutilizzabile. Usando le librerie di tag personalizzati di Struts, gli sviluppatori possono interagire con il resto dell'infrastruttura senza includere codice Java nelle pagine JSP. Questo capitolo presenta una panoramica sulle diverse categorie di tag disponibili all'interno di Struts e spiega come possano rendere lo sviluppo delle applicazioni ancora pi facile. Questo capitolo non vuole rappresentare un riferimento esaustivo per tutti i tag che fanno parte delle librerie di tag in Struts: questo tipo di informazioni pu essere reperito nella guida utente di Struts oppure nei JavaDocs. L'obiettivo di questo capitolo invece quello di esporre i vantaggi dell'impiego delle librerie di tag in Struts e di proporre alcune strategie che possano rendere meno doloroso il passaggio ai tag. Per finire, questo capitolo toccher una nuova iniziativa che circola attraverso il Java Community Process (JCP), intesa a coadiuvare la standardizzazione di alcuni tag custom usati pi comunemente. Questo, a sua volta, render le applicazioni realizzate tramite questi tag ancora pi portabili su diversi container e infrastrutture.
<img src="images/struts-power.gif"/>
I tag possono definire determinati attributi predefiniti che offrono al tag stesso informazioni e che possono influenzare il modo in cui il tag compie le proprie azioni. Nei tag img HTML, ad esempio, l'attributo src definisce il path di un'immagine che sar presentata poi dal tag.
I tag con un body hanno un tag iniziale e un corrispondente tag finale. Tra i due tag vi del contenuto (body).Ecco un esempio di sintassi:
<tagName attributeName="someValue" attributeName2="someValue2"> <!-- The Tag body is between the start and end tags --> </tagName>
I tag con body sono usati per le operazioni sul contenuto del body come formattare output HTML e altro ancora. Ecco un altro esempio di HTML:
<html> <!-- The HTML body inside the start and end HTML tags --> </html>
II tag finale deve sempre cominciare con un carattere "/".
Template C' anche una quinta libreria, la Nested, di cui si parler pi oltre nel capitolo. Le prime versioni di Struts contenevano una libreria di tag chiamata Form. stata poi sostituita dalla libreria HTML, ma in alcuni esempi o nella documentazione si possono trovare ancora riferimenti a tag di Form.
C' un tag per generare una checkbox, uno per un pulsante e un'altro ancora per generare un collegamento (hyperlink).
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
Una volta che siano state incluse queste due lince e che il proprio descrittore di deploy della web application contenga gli indispensabili elementi taglib si possono adoperare tag HTML e Logic all'interno della pagiha JSP. Riprendendo l'esempio precedente, si devono aggiungere le seguenti linee al proprio file web.xml:
property="user.address.city"
viene tradotto nell'equivalente Java:
getUser().getAddress().getCity(
Se si impiega metodo di set su una propriet annidata (come ad esempio quando elaborato un form di input) il metodo di set della propriet invocato nell'ultima propriet della catena. Per il riferimento alla propriet precedente l'espressione equivalente in Java sarebbe:
getUser().getAddress(
).setCity(value)
Le propriet annidate sono molto comode da usare con i tag personalizzati. Sono quasi sempre adoperate unitamente ad attributi di tipo property dei tag supportati
property="address[2]"
in Java:
getAddress(2);
mentre il riferimento alla stessa propriet in un metodo di set sarebbe:
setAddress(2, address)
Come si evince dagli esempi, i subscript dei riferimenti indicizzati sono "relativi a zero" (ovvero, il primo elemento in un array address[0]), proprio come in Java.
Nome tag
Descrizione Visualizza un elemento di base HTML Visualizza un campo-di input (pulsante) Visualizza un pulsante di cancellazione Visualizza un campo di input (checkbox) Visualizza un insieme di messaggi d'errore Visualizza un campo di input per la selezione di un file Definisce un elemento HTML form Visualizza un elemento HTML frame
hidden html image img javascript link messages multibox option options password radio reset rewrite select submit text textarea
Genera un campo nascosto Visualizza un elemento HTML html Visualizza un tag input del tipo "image"Renders an input tag of type "image" Visualizza un tag HTML img Provvede alla validazione di JavaScript basandosi sulle regole di validazione caricate da
ValidatorPlugIn
Visualizza un elemento HTML anchor oppure hyperlink Mostra in modo condizionale un insieme di messaggi cumulativi Visualizza campi di input (checkbox) multipli Visualizza un'opzione select Visualizza una raccolta di opzioni select Visualizza un campo di input per le password Visualizza un campo di input radio button Visualizza un campo di input con pulsante reset Visualizza un URI Visualizza un elemento select Visualizza un pulsante submit Visualizza un campo di input del tipo "text" Visualizza un campo di input textarea
Come precedentemente detto questo capitolo non affronter in dettaglio tutti i tag di Struts; una simile operazione sarebbe inutile dal momento che questa materia trattata in dettaglio nei JavaDoc di Struts. Saranno per selezionati e discussi alcuni tag pi importanti, che possono essere fraintesi dagli sviluppatori e che consentono l'uso di particolari strategie per il loro impiego. Se si necessita dei riferimenti a proposito di un set completo di tag, si possono reperire informazioni nella documentazione delle API del framework Struts all'URL http://jakarta.apache.org/struts/api/.
<html:html locale="true"> <head> <html:base/> <title><bean:message key="title.login"/></title> </head> </html:html> Il tag base di quest'esempio darebbe origine al seguente risultato se eseguito nella pagina principale dell'applicazione
Storefront:
<html lang="en"> <head> <base href="http://localhost:8080/storefront/index.jsp"> <title>Virtual Shopping with Struts</title> </head>
<html>
Questo tag della pi grande importanza quando si adoperano URL relativi per le immagini in una pagina JSP. Non ha associato alcun attributo.
II tag form controlla diversi aspetti importanti della pagina. I suoi attributi sono elencati nella Tabella 8-2.
Tabella 8-2. Gli attributi del tag form
Nome
Descrizione L'URL cui questo form sar sottoposto La codifica del contenuto da usare quando questo form viene passato all'elaborazione. II nome del campo cui sar assegnata la priorit nell'esame della pagina. II metodo HTTP usato per sottoporre la richiesta II nome dell' ActionForm le cui propriet saranno impiegate per il riempimento dei campi di input con i valori appropriati. L'event handler JavaScript che eseguito in seguito a un reset del form. L'event handler JavaScript che eseguito in seguito all'invio del form. Lo scope dell' ActionForm per questo form Gli stili CSS da applicare a questo elemento HTML.
styleClass La classe stylesheet CSS da applicare a questo elemento HTML. styleId L'identificatore da assegnare a questo elemento HTML. target type
II frame target cui questo form inviato. II nome completo della classe dell' ActionForm per questa pagina.
<action path="/signin" type="com.oreilly.struts.storefront.security.LoginAction" scope="request" name="loginForm" validate="true" input="/security/signin.jsp"> <forward name="Success" path="/index.jsp" redirect="true"/> <forward name="Failure" path="/security/signin.jsp" redirect="true"/> </action>
Poniamo ora di avere un tag form come questo dichiarato in una pagina JSP:
<html:form action="signin">
Dal momento che l'attributo name non specificato nel tag form, il tag cercher l'azione signin nel file di configurazione. Recuperato questo valore dall'elemento action lo user per cercare una ActionForm nello scope di sessione o della request. In questo caso sar selezionato loginForm a motivo del proprio valore per l'attributo name nell'elemento action.
<html:html locale="true"> <head> <html:base/> </head> <body> <!<html:form action="searchAction"> <!-- The search fields --> <html:submit styleClass="button" value="Go"/> </html:form> <html:form action="anotherAction"> <!-- The other form fields --> <html:submit styleClass="button"/> </html:form> </body> </html:html>
errors.header=<h3><font color="red">Validation Error</font></h3>You must correct the following error(s) before proceeding:<ul> errors.footer=</ul><hr>
all'interno di una lista. Ora che Struts supporta MessageResources multiple all'interno del file di configurazione, si pu specificare quale deve essere impiegata usando l'attributo bundle.
Attributo
Descrizione Eseguito quando questo elemento perde il focus di input. Eseguito quando questo elemento perde il focus di input ed il suo valore cambiato. Eseguito quando questo elemento riceve un click del mouse.
ondblclick Eseguito quando questo elemento riceve un doppio click del mouse. onfocus Eseguito quando questo elemento riceve un focus di input.onkeydown
Eseguito quando questo elemento ha un focus e il tasto premuto.
onkeypress Eseguito quando questo elemento ha un focus e un tasto premuto e rilasciato. onkeyup Eseguito quando questo elemento ha un focus e un tasto rilasciato. onmousedown Eseguito quando questo elemento sotto il puntatore del mouse ed premuto un tasto. onmousemove Eseguito quando questo elemento sotto il puntatore del mouse e il puntatore spostato. onmouseout Eseguito quando questo elemento sotto il puntatore del mouse ma il puntatore spostato fuori
dall'elemento. l'elemento.
onmouseover Eseguito quando questo elemento non sotto il puntatore del mouse ma il puntatore spostato sopra onmouseup onreset onsubmit
Eseguito quando questo elemento sotto il puntatore del mouse ed rilasciato un tasto del mouse. Eseguito in caso di reset del form che lo contiene. Eseguito in caso di invio del form.
acesskey tabindex
Descrizione Valuta il contenuto annidato di questo tag se la variabile richiesta null o una stringa vuota. Valuta il contenuto annidato di questo tag se la variabile richiesta uguale al valore specificato. Inoltra il controllo alla pagina specificata dall'entry ActionForward. specificato.
greaterEqual Valuta il contenuto annidato di questo tag se la variabile richiesta maggiore o uguale al valore greaterThan Valuta il contenuto annidato di questo tag se la variabile richiesta maggiore del valore specificato.
Reitera il contenuto annidato di questo tag su un insieme specificato. Valuta il contenuto annidato di questo tag se la variabile richiesta minore o uguale al valore specificato. Valuta il contenuto annidato di questo tag se la variabile richiesta minore del valore specificato. Valuta il contenuto annidato di questo tag se il valore specificato una sottostringa specificata della variabile richiesta. richiesta.
messagesNotPresent Genera il contenuto annidato di questo tag se il messaggio specificato non presente nella messagesPresent notEmpty notEqual notMatch notPresent present redirect
Genera il contenuto annidato di questo tag se il messaggio specificato presente nella richiesta. Valuta il contenuto annidato di questo tag se la variabile richiesta non null n una stringa vuota. Valuta il contenuto annidato di questo tag se la variabile richiesta non uguale al valore specificato. Valuta il contenuto annidato di questo tag se il valore specificato non una sottostringa appropriata della variabile richiesta. Genera il contenuto annidato di questo tag se il valore specificato non presente nella richiesta. Genera il contenuto annidato di questo tag se il valore specificato presente nella richiesta. Esegue una redirect HTTP.
I tag all'interno della libreria Logic possono essere divisi in quattro categorie separate sulla base del loro impiego: Comparazione di valori Riconoscimento di sottostringhe Redirect e forward Utilit per la raccolta Questa divisione in categorie effettuata a scopo esplicativo. I tag non sono impacchettati o riuniti in queste categorie; appartengono tutti al package Logic.
Nome
Descrizione II nome di un bean da usare per la comparazione con l'attributo value. Se viene impiegato l'attributo property, il valore confrontato con le propriet del bean invece che con il bean stesso. La variabile da comparare la propriet (del bean specificato dall'attributo name) specificata da quest'attributo, il riferimento alla propriet pu essere semplice, annidato e/o indicizzato. Lo scope all'interno del quale ricercato il bean nominato dall'attributo name. Verr effettuata una ricerca su tutti dli scope se non viene specificato. II valore costante rispetto al quale la variabile comparata, specificata da un'altro attributo di questo tag.
name
parameter II nome di un parametro di richiesta con cui comparare l'attributo value. property scope value
Alcuni esempi saranno d'aiuto a consolidare la conoscenza del modo in cui possano essere usati questi tag di comparazione. Per controllare se un particolare parametro di richiesta presente si pu adoperare il tag di Logic present:
<logic:present parameter="id">
<logic:notEmpty name="userSummary" property="addresses"> <!-- Iterate and print out the user's addresses --> </logic:notEmpty>
Ecco come comparare un valore numerico e una propriet all'interno di una ActionForm:
<logic:lessThan property="age" value="21"> <!-- Display a message about the user's age --> </logic:lessThan>
<logic:matchTag parameter="action" value="processLogin" location="start"> Processing Login.... </logic:matchTag> Se l'attributo location non viene specificato, potrebbe verificarsi una corrispondenza tra la variabile e il valore in una qualsiasi posizione all'interno della variabile stringa.
Usa il valore di questo attributo come un URI relativo a! contesto e genera un URI relativo al server includendo il path del contesto. II tag forward responsabile della redirezione o dell'inoltro a una ActionForward globale specificata. Il tag forward ha un attributo, name, che anche il nome logico dell' ActionForward presa dal file di configurazione.
II nome di un bean JSP con scope di pagina che conterr l'elemento corrente nel corso di un'iterazione.
Un esempio potr agevolare la comprensione dell'uso del tag iterate:
Nome
Descrizione
collection Un'espressione a runtime che valuta un insieme (conforme alle richieste elencate sopra) su cui compiere le iterazioni. id indexed length
II nome di un bean JSP con scope di pagina che contiene l'elemento corrente dell'insieme per ogni iterazione, se non null. II nome di un bean con scope di pagina che contiene l'indice corrente dell'insieme per ciascuna iterazione. II massimo numero di entry (dall'insieme sottostante) su cui compiere le iterazioni nella pagina. Questo potrebbe essere sia un intero che esprime direttamente il valore desiderato o il nome di un bean JSP (in qualunque scope) del tipo java.lang.Integer che definisce il valore desiderato. Se non presente, non esiste limite al numero di iterazioni svolte. II nome del bean che contiene l'insieme su cui compiere iterazioni (se property non specificato), o il bean JSP le cui propriet di get restituiscono l'insieme su cui compiere le iterazioni (se specificato property). L'indice relativo del punto di partenza dell'insieme su cui saranno svolte le iterazioni. Pu essere un intero che esprime direttamente il valore voluto o il nome di un bean JSP (in qualsiasi scope) del tipo java.lang.Integer che definisce il valore desiderato. Se non presente, sar posto a zero (le iterazioni saranno svolte dall'inizio dell'insieme). II nome della propriet del bean JSP specificato da name il cui metodo getter restituisce l'insieme su cui svolgere le iterazioni. Lo scope del bean all'interno del quale cercare il bean citato dalla propriet name o "qualsiasi scope" se non specificato. II nome completo della classe dell'elemento da esporre attraverso il bean JSP citato dall'attributo id. Se non presente, non sar effettuata nessuna conversione di tipi. Gli elementi dell'insieme devono poter essere assegnabili a questa classe, altrimenti si verificher un'eccezione ClassCastException.
name
Nome
Descrizione Il parametro chiave per recuperare il messaggio dallo scope di request. recuperati tutti i messaggi (ignorando la propriet).
name
property II nome della propriet per la quale i messaggi dovrebbero essere recuperati. Se non specificato, saranno
Per default, il tag prender il bean con scope di richiesta che iterer sulla stringa costante Action.ERROR_KEY, ma se quest'attributo impostato a true, il bean con scope di richiesta sar preso message dalla stringa costante Action.MESSAGE_KEY Inoltre, se impostato a true, qualsiasi valore assegnato all'attributo name sar ignorato.
Descrizione Definisce una variabile di scripting sulla base del valore di uno specifico request cookie. Definisce una variabile di scripting sulla base del valore di uno specifico bean property. Definisce una variabile di scripting sulla base del valore di uno specifico request header. Carica la risposta da una richiesta dinamica di un'applicazione e la rende disponibile come bean. Nella risposta mostra un messaggio internazionalizzato. Espone come bean un oggetto dal contesto della pagina specificata. Carica una risorsa di una web application e la rende disponibile come bean. Definisce un bean che contiene il numero degli elementi in una Collection o in una Map. Espone un oggetto della configurazione interna di Struts come bean. Scrive il valore della propriet specificata del bean. Molti tag di questa libreria lanciano eccezioni del tipo JspException quando sono usate non correttamente (p.e., quando si specifica una combinazione non valida degli attributi dei tag). Le JSP permettono di dichiarare una "pagina di errore" nella direttiva <%@ page %> . Se si desidera elaborare l'eccezione che ha causato il problema, questa passata alla pagina di errore come un'attributo della richiesta con la dicitura org.apache.struts.action.EXCEPTION.
cookie define header include message page resource size struts write
parameter Definisce una variabile di scripting basata sul/i valore/i del parametro di richiesta specificato.
<td><bean:message key="global.user.firstName"/>:</td>
Il concetto di template non stato ancora, approfondito in questo libro. La libreria Template mette a disposizione tag utili per la creazione di template dinamici JSP per pagine che condividono un formato simile. Questi tag offrono capacit simili agli stylesheet o alla direttiva standard include nella tecnologia JSP ma possono essere anche dinamici. Con l'avvento dell'infrastruttura Tiles che sar affrontata nel Capitolo 14, i tag di Template non sono pi molto usati. Per questo si elencheranno i tag della libreria ma non si entrer nel dettaglio. Se si desideranti maggiori informazioni, possibile trovarle all'URL http://jakarta.apache.org/struts/api/. Ecco l'elenco dei tag della libreria dei tag Template nella Tabella 8-10.
Tabella 8-10. Tag personalizzati nella libreria Template
Descrizione Recupera (o include) il file template speciticato e inserisce il contenuto scelto nel template. Modificando l'aspetto definito nel file template, qualsiasi altro file inserito nel body del template user il nuovo aspetto. Crea un bean con scope di request che specifica il contenuto da usare per il tag get. Il contenuto pu essere direttamente scritto o incluso da una JSP o da un file HTML. Recupera il contenuto da un bean con scope di request per usarlo nel layout di un template
Comunque, i componenti di Tiles offrono maggiori funzionalit rispetto a questi tag. C' chi ancora li usa, ma la maggior parte degli sviluppatori sta migrando verso Tiles.
codice sorgente, il che permette non solo una migliore comprensione di come si sviluppano i tag JSP ma anche di modificare o estendere i tag a seconda delle esigenze della propria organizzazione. Non ha senso elencare i tag disponibili, dal momento che se ne aggiungono di nuovi con regolarit. La miglior cosa dare un'occhiata al sito all'indirizzo http://jakarta.apache.org/taglibs/.
8.9.2 JSPTags.com
Un'altro utile silo web che contiene molti tag delle JSP http://jsptags.com. Questo sito in piedi da parecchi anni e contiene molte librerie di tag a disposizione gratuitamente ma anche una fonte autorevole di informazioni sulle web application. Ci si accerti effettuare un controllo su questo sito prima di intraprendere la creazione di un proprio tag. Qualcuno potrebbe aver gi creato il tag di cui si ha bisogno, risparmiandoci il tempo di sviluppo e di test.
CAPITOLO 9
Uno dei maggiori vantaggi dell'impiego di un framework sta nella possibilit di estenderlo e personalizzarlo sulla base delle necessit dell'applicazione. Il framework Struts non fa eccezione: offre molteplici e importanti punti di estensione per gli sviluppatori. Questo capitolo getta luce su alcuni di questi punti di estensione e discute i pro e i contro dell'operazione di estensione del framework.
public interface PlugIn { /** * Notification that the specified application module is being started. */ public void init(ActionServlet servlet, ApplicationConfig config) throws ServletException; /** * Notification that the application module is being shut down. */ public void destroy( ); }
Durante l'avvio di un'applicazione Struts, ActionServlet invoca il metodo init() per ogni PlugIn configurata; il framework supporto la configurazione di una o pi PlugIn per applicazione. Le routine di inizializzazione necessarie al proprio plug-in dovrebbero essere svolte nel corso del metodo init(): un buon momento per inizializzare una connessione a un database o, ad esempio, per stabilire una connessione a un sistema remoto.[1]
[1]
Il secondo metodo che il proprio plug-in deve implementare destroy(). Il framework invoca questo metodo quando l'applicazione viene arrestata. La necessaria pulizia andrebbe effettuata in questo frangente: il momento adatto per chiudere le connessioni al database, le socket remote e qualsiasi altra risorsa che il plug-in stia usando. Ecco un esempio concreto dell'uso di PlugIn in Struts. Si supponga che l'applicazione debba comunicare con uno strato EJB. Una delle prime cose da fare fare prima che possa avvenire questa comunicazione sta nell'ottenere un riferimento al servizio Java Naming and Directory Interface (JNDI). JNDI permette ai client di accedere a svariati servizi di nomi e directory come datasource, sessioni JavaMail e factory di EJB home. L'Esempio 9-2 mostra l'acquisizione conPlugIn di un InitialContext per un servizio JNDI.
Esempio 9-2. Un esempio del meccanismo PlugIn di Struts
package com.oreilly.struts.storefront.framework.ejb; import import import import import import import java.util.HashTabella; javax.naming.InitialContext; javax.naming.Context; org.apache.struts.action.ActionServlet; org.apache.struts.config.ApplicationConfig; org.apache.struts.action.PlugIn; javax.servlet.ServletException;
public class JNDIConnectorPlugin implements PlugIn { private String jndiFactoryClass; private String jndiURL; private Context initCtx = null; public JNDIConnectorPlugin( super( ); } ) {
public void init(ActionServlet servlet, ApplicationConfig config) throws ServletException{ // Get the host and port where the JNDI service is running jndiFactoryClass = servlet.getInitParameter("jndi-factory-class"); jndiURL = servlet.getInitParameter("jndi-url"); try{ HashTabella props = new HashTabella( ); // The EJB spec also allows these to be read from the jndi.properties file props.put( Context.INITIAL_CONTEXT_FACTORY, jndiFactoryClass );
props.put( Context.PROVIDER_URL, jndiURL ); initCtx = new InitialContext(props); }catch( Exception ex ){ throw new ServletException( ex ); } // Store the JNDI Context into the ServletContext servlet.getServletContext( ).setAttribute( "Storefront.InitCtx", initCtx ); } public void destroy( ){ try{ if ( initCtx != null ){ initCtx.close( ); initCtx = null; // No need to remove from ServletContext because app is being shut down } }catch( Exception ex ){ ex.printStackTrace( ); } } }
Quando il framework invoca il metodo init() della classe JNDIConnectorPlugin, il plug-in crea un oggetto InitialContext e lo conserva all'interno del ServletContext. Questo permette all' InitialContext di JNDI di essere usato dall'intera applicazione quando ve ne bisogno. Si tratta solo di un semplice esempio; ci sono molti possibili impieghi del meccanismo PlugIn. Per esempio, il framework Validator, che sar trattato nel Capitolo 11, adopera il meccanismo PlugIn per inizializzare le regole di validazione di un'applicazione.
Se si specifica pi di un elemento plug-in, questi saranno inizializzati nell'ordine in cui sono elencati nel file di configurazione
Per maggiori dettagli sulla configurazione dell'elemento plug-in o per passare delle propriet ad un'istanza si veda "Il DTD di configurazione di Struts" nel Capitolo 4.
Per esempio si supponga di voler passare un parametro aggiuntivo alle proprie classi Action. Per default si impiega la classe ActionMapping che estende ActionConfig dal package config. Per trasmettere un parametro aggiuntivo come ssl-required che controlla se usato HTTP o HTTPS, si pu estendere la classe ActionMapping e configurare questa estensione attraverso l'attributo className. La possibilit di estendere gli elementi di configurazione di Struts attraverso questo meccanismo rende il framework estremamente duttile e flessibile, tanto da poter venire incontro alle necessit di qualsiasi applicazione.
In maniera predefinita non compie alcuna azione nelle request ma possibile usarlo in vari modi per modificare il funzionamento della normale elaborazione delle richieste. Il metodo processPreprocess() appare come:
protected boolean processPreprocess( HttpServletRequest request, HttpServletResponse response ){ boolean continueProcessing = true; // Get the name of the remote host and log it String remoteHost = request.getRemoteHost( ); log.info( "Request from host: " + remoteHost ); // Make sure the host is from one that you expect if ( remoteHost == null || !remoteHost.startsWith( "127.") ){ // Not the localhost, so don't allow the host to access the site continueProcessing = false; ForwardConfig config = appConfig.findForwardConfig("Unauthorized"); try{ response.sendRedirect( config.getPath( ) ); }catch( Exception ex ){ log.error( "Problem sending redirect from processPreprocess( )" ); } } return continueProcessing; }
La cosa pi importante da notare in questo esempio che anche se il metodo processPreprocess() restituisce false, sta ancora a questo metodo prendersi carico della redirect della richiesta. La restituzione di false informa il RequestProcessor solo del fatto che non deve continuare. Il controller non compie alcuna azione con la request; la responsabilit passa al metodo processPreprocess().
Il modo in cui l'Esempio 9-3 specifica il path nel metodo sendRedirect() ci evita di inserire direttamente nel codici, l'URI della risorsa unauthorized_access.jsp In questo modo, se la pagina per effettuare il forward cambia, questo metodo non dovr essere modificato.
Ci sono altri due modi in cui si pu operare la stessa logica senza impiegare il metodo processPreprocess(). Un'alternativa consiste nell'usare una servlet filtro (che fa parte delle API Servlet 2.3). I filtri permettono di ispezionare la richiesta persino prima che raggiunga il controller di Struts. Tuttavia ci sono due problemi di cui essere consapevoli. In primo luogo, dal momento che i filtri sono parte delle API 2.3 non potranno essere usate se si usa un servlet container che supporta soltanto la versione 2.2. In secondo luogo, dal momento che il filtro ispeziona la request al principio dello stadio di elaborazione, i filtri non hanno un accesso molto facile alle API di Struts. Questo rende difficoltoso ricercate ActionForward o qualunque altro sistema che che normalmente si usa nel metodo processPreprocess().
Questo avviene perch, dal momento che il controller di Struts non ha ancora ricevuto la richiesta nel momento in cui il filtro la ispeziona, esso non ha avuto la possibilit di selezionare un modulo applicativo appropriato.
import import import import import import import import import import import
java.util.Collection; java.util.LinkedList; java.util.List; java.util.Locale; java.util.Iterator; javax.servlet.http.*; org.apache.struts.action.*; com.oreilly.struts.storefront.framework.util.IConstants; com.oreilly.struts.storefront.framework.exceptions.*; com.oreilly.struts.storefront.framework.UserContainer; com.oreilly.struts.storefront.service.IStorefrontService;
/** * An abstract Action class that all Storefront Action classes can extend. */ abstract public class StorefrontBaseAction extends Action { /** * The default execute( ) method that all actions must implement. */ public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{ // It just calls a worker method that contains the real execute logic return executeAction( mapping,form,request,response,getUserContainer(request)); } /** * The actual do work method that must be overridden by the subclasses. */ abstract public ActionForward executeAction( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response, UserContainer userContainer ) throws Exception; // This super Action is also a good place to put common utility methods public boolean isLoggedIn( HttpServletRequest request ){ UserContainer container = getUserContainer(request); return ( container != null && container.getUserView( ) != null ); } /** * Retrieve the UserContainer for the user tied to the request. */ protected UserContainer getUserContainer(HttpServletRequest request) { HttpSession session = request.getSession( );
UserContainer userContainer = (UserContainer)session.getAttribute(IConstants.USER_CONTAINER_KEY); // Create a UserContainer if one doesn't exist already if(userContainer == null) { userContainer = new UserContainer( ); userContainer.setLocale(request.getLocale( )); session.setAttribute(IConstants.USER_CONTAINER_KEY, userContainer); } return userContainer; } }
La classe StorefrontBaseAction dell'Esempio 9-4 mostra come si possa impiegare Action per svolgere un funzionamento ripetitivo di modo che tutte le sottoclassi non debbano svolgerlo per conto loro. Per esempio, si supponga che tutte le proprie classi Action debbano avere UserContainer per l'utente corrente e debbano poter usare delle informazioni al suo interno (ad esempio, ID dell'utente o permessi di sicurezza). Da una parte si potrebbero forzare tutte le classi Action a ottenere UserContainer per loro conto, gestire la situazione in caso non ve ne sia uno e cos via. Un modo alternativo, maggiormente gestibile, consiste nel porre quella funzionalit in una superclasse Action e nel passare UserContainer alle sottoclassi. Come riportato dall' Esempio 9-4StorefrontBaseAction implementa il metodoexecute() ma all'interno di quel metodo prende un'istanza di UserContainer e la passa come argomento al metodo executeAction(). Ogni sottoclasse implementa il metodo astratto executeAction() e fa passare, istanzia e garantisce che non sia "null" UserContainer. Questo solo un banale esempio di quello che si pu fare. Qualsiasi funzionamento che le classi debbano svolgere un candidato per la propria implementazione nella superclasse Action, cos, quando giunge l'occasione di modificare l'implementazione, deve essere modificato solo il funzionamento nella superclasse.
Tuttavia ci sono due classi che potrebbero appartenere alla categoria dei componenti del model che possibile estendere. Non sono certo rappresentative della sostanza di un model ma sono responsabili del mantenimento dello stato del model.
package com.oreilly.struts.storefront.framework; import java.util.Locale; import javax.servlet.http.HttpSessionBindingListener; import javax.servlet.http.HttpSessionBindingEvent; import com.oreilly.struts.storefront.customer.view.UserView; /** * Used to store information about a specific user. This class is used * so that the information is not scattered throughout the HttpSession. * Only this object is stored in the session for the user. This class * implements the HttpSessionBindingListener interface so that it can * be notified of session timeout and perform the proper cleanup. */ public class UserContainer implements HttpSessionBindingListener { // The user's shopping cart private ShoppingCart cart = null; // Data about the user that is cached private UserView userView = null; /** * The Locale object for the user. Although Struts stores a Locale for * each user in the session, the locale is also maintained here. */ private Locale locale; public UserContainer( super( ); initialize( ); } ) {
) {
public void setCart(ShoppingCart newCart) { cart = newCart; } public void setLocale(Locale aLocale) { locale = aLocale; } public Locale getLocale( return locale; } ) {
/** * The container calls this method when it is being unbound from the * session. */ public void valueUnbound(HttpSessionBindingEvent event) { // Perform resource cleanup cleanUp( ); } /** * The container calls this method when it is being bound to the * session. */ public void valueBound(HttpSessionBindingEvent event) { // Don't need to do anything, but still have to implement the // interface method. } public UserView getUserView( return userView; } ) {
public void setUserView(UserView newView) { userView = newView; } /** * Initialize all of the required resources */ private void initialize( ) { // Create a new shopping cart for this user cart = new ShoppingCart( ); } /** * Clean up any open resources. The shopping cart is left intact * intentionally. */ public void cleanUp( ) { setUserView( null ); } }
Una
cosa da notare che la classe UserContainer dell'Esempio 9-5 implementa l'interfaccia HttpSessionBindingListener, I metodi di quest'interfaccia permettono all' UserContainer di essere avvisati quando esso legato o meno alla sessione. Questo gli permette di compiere qualsiasi ripulitura dell'oggetto necessaria. Viene creata un'istanza di UserContainer per ciascun nuovo utente nel momento in cui esso entra nell'applicazione. L'oggetto UserContainer di per s stesso conservato nella sessione e deve avere la durata di questa. Se un qualsiasi utente esce e rientra nel sito, di solito viene creato un nuovo UserContainer. La ApplicationContainer ha pressappoco un utilizzo analogo ma a livello applicazione e nondi sessione. La sua utilit consiste nel poter conservare o fare il caching di informazioni dell'applicazione necessarie a tutti gli utenti. La classe ApplicationContainer opera con elementi come elenchi di selezione, propriet di configurazione e altri dati non
specifici che una volta ricevuti devono essere solo conservati. Questa classe viene creata quando l'applicazione avviata la prima volta e distrutta quando l'applicazione viene arrestata.
CAPITOLO 10
Lo spazio non permette di mostrare tutti i discendenti della classe Throwable che solamente nella libreria fondamentale Java sono pi di cento fra sottoclassi dirette e indirette. Di solito le eccezioni discendenti da Exception sono lanciate per indicare condizioni anormali che possono di solito essere gestite dall'applicazione. Tutte le eccezioni create e lanciate dalla propria applicazione Struts dovrebbero essere sottoclassi della classe Exception, Le eccezioni che discendono dall'altro ramo di Throwable, ovvero che derivano dalla classe Error, sono riservate a problemi pi seri che capitano durante il ciclo di vita dell'applicazione. Per esempio, se non c' pi memoria disponibile per un'applicazione, sar generato un messaggio OutOfMemoryError e di solito nessun client pu far niente per questo. Per questo i client di solito non si preoccupano della gestione delle sottoclassi di Error. Nella maggior parte dei casi la stessa JVM che lancia delle istanze di Error o delle sue sottoclassi.
Quando un metodo Java termina correttamente, la JVM elimina la porzione della pila relativa a tale metodo prosegue con l'elaborazione del metodo precedente dal punto in cui era stata lasciata. Quando si verifica la condizione per un'eccezione, tuttavia, la JVM deve trovare un appropriato gestore delle eccezioni. Dapprima controlla se il metodo corrente "cattura" l'eccezione. Se la cattura, l'esecuzione continua nell'ambito definito dalla clausola di catch. Se il metodo corrente non offre la possibilit di catturare l'eccezione, la JVM inizier a gettare fuori dalla pila di chiamata i frame dei metodi finch non trovi un gestore per 1' eccezione o per una delle eccezioni gerarchicamente superiori che hanno generato l'eccezione in oggetto. Alla fine, se non trova un gestore per le eccezioni neanche se risale al metodo main(), il thread terminer. Se il thread quello principale e non ci sono altri thread in background, terminer l'applicazione stessa. Se la JVM trova un gestore delle eccezioni, l'elaborazione riprender da quel metodo, che viene posto in cima allo stack. L'importanza di sapere come la JVM gestisce le eccezioni consiste nel fatto che molte di esse, quando si verificano, discendono molto in profondit. Per la JVM potrebbe essere un lavoro durissimo localizzare un gestore delle eccezioni per una particolare eccezione specie se il gestore posto a un basso livello dello stack. Diventa molto importante perci inserire a livelli appropriati degli exception handler. Se ci si lasciano scappare le eccezioni, facile che finiscano per bloccare l'applicazione.
client a decidere cosa far quando accadono delle condizioni anormali. Tuttavia, come si capir dalla prossima sezione, nessuna eccezione Java uguale ad un'altra.
Double basePrice = null; String basePriceStr = request.getParameter( "BASE_PRICE_AMOUNT" ); // Use a try/catch to make sure the value is a number try{ basePrice = Double.valueOf( basePriceStr ); }catch( NumberFormatException ex ){ // The value could not be converted to a valid Double; set the default basePrice = ApplicationDefaults.DEFAULT_BASE_PRICE; }
Il codice precedente mostra un blocco try/catch che determina una condizione d'errore e prende dei provvedimenti correttivi. La condizione un valore non valido di prezzo e l'azione correttiva consiste nell'assegnargli un valore prestabilito. Ci sono altri modi per determinare se una stringa sia un valore Double valido ma un approccio del genere molto sfruttato. Fortunatamente il gestore delle eccezioni nello stesso metodo e la JVM non viene messa in difficolt se ci si verifica. Certo, le regole sono sempre in qualche modo soggettive e quella che una ragione valida per uno sviluppatore potrebbe, non esserla per un'altro. Occorre essere consapevoli dei problemi ed evitare blocchi try/catch se non per vere condizioni di errore.
utente cerca di fare login in un'applicazione ma l'account stato bloccato. Questo non rappresenta un errore catastrofico ma comunque un problema che deve essere registrato e gestito. All'interno di applicazioni Struts e all'interno delle web application in generale ci sono essenzialmente due approcci da adottare ogniqualvolta si verifichi un'eccezione. Se si tratta di un'eccezione di applicazione che l'utente pu in qualche modo gestire, di solito si restituir il controllo alla pagina di input e si visualizzer una descrizione di facile comprensione e un'azione alternativa per risolverla. Continuando con l'esempio dell'account bloccato, si potrebbe lanciare un'eccezione AccountLockedException agganciata alla classe action che poi restituisce il controllo alla pagina di login e informa che l'account bloccato. Se l'eccezione lanciata un'eccezione di basso livello come RemoteException, la sola azione significativa che l'applicazione pu svolgere mostrare una pagina di errore di sistema. Non c' niente che l'utente possa fare per risolvere il problema. Potrebbe trattarsi di un errore di programmazione o qualche problema di rete ma il punto che non si desidera che l'utente veda la traccia della pila dell'eccezione. Piuttosto potrebbe essere il caso di indirizzare verso una pagina di errore di sistema pi comprensibile ed eventualmente si informi l'utente di darne avviso all'amministratore. L'eccezione dovrebbe anche essere registrata per determinare la causa del problema. Pi oltre nel capitolo si vedranno esempi su come restituire il controllo alla pagina di input e mostrare un errore localizzato all'utente. Si vedr anche come si dirigono i messaggi d'errore a una pagina di errore di sistema, tutte cose che aggiungono valore all'applicazione e arricchiscono l'esperienza dell'utente.
import java.io.PrintStream; import java.io.PrintWriter; /** * This is the common superclass for all application exceptions. This * class and its subclasses support the chained exception facility that allows * a root cause Throwable to be wrapped by this class or one of its * descendants. */ public class BaseException extends Exception { protected Throwable rootCause = null; protected BaseException( Throwable rootCause ) { this.rootCause = rootCause; } public void setRootCause(Throwable anException) { rootCause = anException; } public Throwable getRootCause( return rootCause; ) {
} public void printStackTrace( ) { printStackTrace(System.err); } public void printStackTrace(PrintStream outStream) { printStackTrace(new PrintWriter(outStream)); } public void printStackTrace(PrintWriter writer) { super.printStackTrace(writer); if ( getRootCause( ) != null ) { getRootCause( ).printStackTrace(writer); } writer.flush( ); } }
La classe dell'Esempio 10-1 permette di effettuare il wrapping della Throwable originale con un'istanza di questa eccezione o di tutti i suoi derivati. Il bello di questa caratteristica sta nel permettere di astrarre i dettagli di basso livello delle applicazioni e allo stesso tempo di mantenere questi dettagli disponibili in modo che possano essere scritti in un log e usati dagli sviluppatori. Dal momento che queste eccezioni possono essere raggruppate indefinitamente, ci si riferisce a questo concetto come a quello di concatenamento delle eccezioni. Il concatenamento delle eccezioni un modo eccellente per evitare che astrazioni di livello basso, come l'accesso JDBC, si propaghino all'utente finale. L'utente finale non si cura del problema di basso livello e astrarre, il problema a un livello pi alto gli impedir di vedere buona parte dei dettagli. Un'altro beneficio dell'impiego di una classe di eccezioni a livello alto sta nel fatto che le API dello strato superiore non sono legate o accoppiate ai dettagli della loro implementazione. Sulla stessa falsariga dell'esempio precedente si supponga che un filesystem sia usato inizialmente per conservare le immagini per cui la clausola throws del metodo updateImageFile() dichiara di lanciare una IOException. Se in seguito l'implementa zione cambia, e viene usato JDBC, la clausola di throws e il client che invoca il metodo devono essere cambiati per dichiarare o raccogliere una SQLException. Con un maggior livello di astrazione il client necessita solo di curarsi della UploadException, qualunque sia l'implementazione sottostante.
package com.oreilly.struts.framework.exceptions; import java.util.List; import java.util.ArrayList; import java.io.PrintStream; import java.io.PrintWriter; /** * This is the common superclass for all application exceptions. This
* class and its subclasses support the chained exception facility that allows * a root cause Throwable to be wrapped by this class or one of its * descendants. This class also supports multiple exceptions via the * exceptionList field. */ public class BaseException extends Exception{ protected Throwable rootCause = null; private List exceptions = new ArrayList( public BaseException( super( ); } ){ );
public BaseException( Throwable rootCause ) { this.rootCause = rootCause; } public List getExceptions( return exceptions; } ) {
public void addException( BaseException ex ){ exceptions.add( ex ); } public void setRootCause(Throwable anException) { rootCause = anException; } public Throwable getRootCause( return rootCause; } ) {
public void printStackTrace( ) { printStackTrace(System.err); } public void printStackTrace(PrintStream outStream) { printStackTrace(new PrintWriter(outStream)); } public void printStackTrace(PrintWriter writer) { super.printStackTrace(writer); if ( getRootCause( ) != null ) { getRootCause( ).printStackTrace(writer); } writer.flush( ); } }
Si noti nell'Esempio 10-2 che stata aggiunta alla classe una java.util.List. Se si verifica pi di una eccezione durante l'elaborazione di un metodo, le eccezioni in pi possono essere aggiunte e restituite al client. Se Se il client vuole trattare solo una eccezione non dovr raccogliere tutte quelle addizionali
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <action-mappings> <action path="/login" type="com.oreilly.struts.storefront.security.LoginAction" name="loginForm" scope="request" input="/login.jsp"> <!--The following exceptions can be thrown during the login action --> <exception key="security.error.changepassword" path="/changePassword.jsp" type="com.oreilly.struts.framework.exceptions.ExpiredPasswordException "/>
<exception key=" security.error.loginfailed" type="com.oreilly.struts.framework.exceptions.InvalidLoginException" path="/login.jsp"/> <exception key="security.error.accountlocked" type="com.oreilly.struts.framework.exceptions.AccountLockedException" path="/accountLocked.jsp"/> </action> </action-mappings> </struts-config>
L'elemento exception definito nel mapplng delle azioni o anche nella sezione delle eccezioni globali specifica il path verso cui indirizzarsi quando si verifica una delle eccezioni durante la corrispondente invocazione idi azione. Per esempio, se viene lanciata l'eccezione ExpiredPasswordException durante l'azione login, il controller passer il controllo alla pagina changePassword.jsp.Presumibilmente se viene lanciata AccountLockedException, il controllo verr passato alla pagina accountLocked.jsp. Quando un'eccezione non gestita a livello di programmazione nella classe Action, la RequestProcessor prova a vedere se esiste un elementoexception configurato per quellospecifico tipo di eccezione. Se tale elemento esiste, il controllo passato alla risorsa specificata nell'attributo path di exception. L'Esempio 10-4 mostra il metodo processException() della classe RequestProcessor.
Esempio 10-4. Il metodo processException() della RequestProcessor di Struts
protected ActionForward processException(HttpServletRequest request, HttpServletResponse response, Exception exception, ActionForm form, ActionMapping mapping) throws IOException, ServletException { // Is there a defined handler for this exception? ExceptionConfig config = mapping.findException(exception.getClass( ));
if (config == null){ if (log.isDebugEnabled( )){ log.debug(getInternal().getMessage("unhandledException",exception.getClas s( ))); } if (exception instanceof IOException){ throw (IOException) exception; }else if (exception instanceof ServletException){ throw (ServletException) exception; }else{ throw new ServletException(exception); } } // Use the conFigurad exception handling try { Class handlerClass = Class.forName(config.getHandler( )); ExceptionHandler handler = (ExceptionHandler)handlerClass.newInstance( ); return (handler.execute(exception, config, mapping, form,request, response)); }catch (Exception e){ throw new ServletException(e); } }
Si noter che il metodo findException() potrebbe restituire un oggettoExceptionConfig all'inizio del metodo processException(). L'oggetto ExceptionConfig una rappresentazione in memoria dell'elemento exception specificato all'interno del file di configurazione. Se il metodo findException() non trova un elemento exception per lo specifico tipo di eccezione verificatasi, l'eccezione rinviata al client senza passare
attraverso il gestore delle eccezioni di Struts. A meno che l'eccezione sia una IOException oppure una delle sue sottoclassi, questa eccezione dunque sar incapsulata da una istanza di ServletException e rilanciata. Se c' un elemento exception specificato nel mapping delle azioni per lo specifico tipo di eccezione verr restituito un oggetto ExceptionConfig dal metodo findException(). Il metodo getHandler() poi invocato sull'oggetto ExceptionConfig e viene usato il gestore assegnato l'elaborazione dell'eccezione. per Struts possiede una classe predefinita per l'elaborazione delle eccezioni, usata a meno che non se ne configuri una propria. La classe predefinita per la gestione delle eccezioni org.apache.struts.action.ExceptionHandler. Il metodo execute() di questo handler crea un ActionError, lo conserva all'interno di uno scope appropriato e restituisce un oggetto ActionForward associato con l'attributo path specificato nell'elemento exception. Per riassumere, se si dichiara un elemento exception all'interno di un elemento action, il gestore delle eccezioni predefinito creer e conserver un ActionError all'interno dello scope specificato passando il controllo alla risorsa specificata dall'attributo path. Come si notato nel Capitolo 4, l'elemento exception permette anche di ridefinire il funzionamento standard del gestore delle eccezioni se si desidera una risposta diversa quando si verifica una certa eccezione. Specificando un nome completo di classe Java che estenda la classe org.apache.struts.action.ExceptionHandler nell'attributo dell'eccezione handler, il metodo execute() di ExceptionHandler verr ridefinito affinch svolga il compito specificato. Per esempio, le eccezioni di un'applicazione potrebbero estendere la classe BaseException come illustrato dall'Esempio 10-5.
Esempio 10-5. Classe di eccezione che supporta una chiave di messaggio e suoi argomenti
package com.oreilly.struts.framework.exceptions; import java.util.List; import java.util.ArrayList; import java.io.PrintStream; import java.io.PrintWriter; /** * This is the common superclass for all application exceptions. This * class and its subclasses support the chained exception facility that allows * a root cause Throwable to be wrapped by this class or one of its * descendants. This class also supports multiple exceptions via the * exceptionList field. */ public class BaseException extends Exception{ protected Throwable rootCause = null; private List exceptions = new ArrayList( private String messageKey = null; private Object[] messageArgs = null; public BaseException( super( ); } ){ );
public BaseException( Throwable rootCause ) { this.rootCause = rootCause; } public List getExceptions( return exceptions; } ) {
public void addException( BaseException ex ){ exceptions.add( ex ); } public void setMessageKey( String key ){ this.messageKey = key; } public String getMessageKey( return messageKey; } ){
public void setMessageArgs( Object[] args ){ this.messageArgs = args; } public Object[] getMessageArgs( return messageArgs; } ){
public void setRootCause(Throwable anException) { rootCause = anException; } public Throwable getRootCause( return rootCause; } ) {
public void printStackTrace( ) { printStackTrace(System.err); } public void printStackTrace(PrintStream outStream) { printStackTrace(new PrintWriter(outStream)); } public void printStackTrace(PrintWriter writer) { super.printStackTrace(writer); if ( getRootCause( ) != null ) { getRootCause( ).printStackTrace(writer); } writer.flush( ); } }
La classe BaseException dell'Esempio 10-5 contiene un messageKey utilizzabile come chiave nel resource bundle di Struts. Questa chiave pu essere passata al costruttore della classe ActionError e il framework la far corrispondere a un messaggio nel resource bundle di Struts. Questa classe contiene anche un array di oggetti che pu essere popolato dal creatore dell'eccezione. Questi oggetti possono poi essere utilizzati per la sostituzione all'interno di un messaggio da parte del bundle che contiene parametri di sostituzione basati sulla classe MessageFormat. Ecco un esempio di messaggio nel bundle:
package com.oreilly.struts.chapter10Esempios; import import import import import import import import import import javax.servlet.ServletException; javax.servlet.http.HttpServletRequest; javax.servlet.http.HttpServletResponse; org.apache.struts.action.ExceptionHandler; org.apache.struts.action.ActionForm; org.apache.struts.action.ActionError; org.apache.struts.util.AppException; org.apache.struts.action.ActionForward; org.apache.struts.action.ActionMapping; org.apache.struts.config.ExceptionConfig;
protected ActionForward execute(Exception ex, ExceptionConfig config, ActionMapping mapping, ActionForm formInstance, HttpServletRequest request, HttpServletResponse response) throws ServletException { ActionForward forward = null; ActionError error = null; String property = null; /* Get the path for the forward either from the exception element * or from the input attribute. */ String path = null; if (config.getPath( ) != null) { path = config.getPath( ); }else{ path = mapping.getInput( ); } // Construct the forward object forward = new ActionForward(path); /* Figura out what type of exception has been thrown. The Struts * AppException is not being used in this Esempio. */ if( ex instanceof BaseException) { // This is the specialized behavior BaseException baseException = (BaseException)ex; String messageKey = baseException.getMessageKey( ); Object[] exArgs = baseException.getMessageArgs( ); if ( exArgs != null && exArgs.length > 0 ){ // If there were args provided, use them in the ActionError error = new ActionError( messageKey, exArgs ); }else{ // Create an ActionError without any arguments error = new ActionError( messageKey ); } }else{ error = new ActionError(config.getKey( )); property = error.getKey( ); } // Store the ActionError into the proper scope // The storeException method is defined in the parent class storeException(request, property, error, forward, config.getScope( )); return forward; } }
Sta allo sviluppatore definire il funzionamento specializzato che si pone in opera nella propria classe di gestione delle eccezioni. Nell'Esempio 10-6, inserendo degli argomenti in ActionError, il messaggio viene reso maggiormente comprensibile. Per informazioni pi dettagliate sull'installazione di un gestore delle eccezionisi consulti il paragrafo "Il DTD di configurazione di Struts" nel Capitolo 4.
Ci sono molti altri casi in cui sar necessario ignorare il funzionamento predefinito. Questo non prevede infatti un oggetto di eccezione che supporti eccezioni multiple. Se la propria applicazione necessita del supporto per questa funzionalit si dovranno creare le proprie classi ExceptionHandler.
Nella maggior parte dei casi il gestore delle eccezioni di Struts sufficiente a soddisfare le esigenze. Solo quando vi necessit di una gestione specializzata delle eccezioni che non pu essere ottenuta mediante il gestore delle eccezioni di Struts ci si dovrebbe impegnare a crearne una propria. La Figura 10-3 illustra un diagramma di sequenza della gestione delle eccezioni standard di Struts.
L'uso del meccanismo dichiarativo di gestione delle eccezioni di Struts non preclude l'uso di un approccio programmatico. Di fatto, i due sistemi possono funzionare abbastanza bene in combinazione. Le classi Action proveranno dapprima a gestire una certa eccezione e solo se essa non raccolta e gestita dall'istanza di Action sar raccolta dal metodo processActionPerform() nella classe RequestProcessor. La RequestProcessor impiegher allora il meccanismo dichiarativo di gestione delle eccezioni per elaborare l'errore. Di seguito si tratta appunto il modo di gestire le eccezioni mediante approccio programmatico.
try{ // Peform some work that may cause an application or system exception }catch( BaseException ex ){ // Log the exception // Create and store the action error ActionErrors errors = new ActionErrors( ); ActionError newError = new ActionError( ex.getErrorCode(), ex.getArgs( ) ); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors ); // Return an ActionForward for the Failure resource return mapping.findForward( "Failure" ) }catch( Throwable ex ){ // Log the exception // Create and store the action error ActionError newError = new ActionError( "error.systemfailure" ); ActionErrors errors = new ActionErrors( ); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors ); // Return an ActionForward for the system error resource return mapping.findForward( IConstants.SYSTEM_FAILURE_PAGE ); }
Il problema che all'interno di ogni classe Action si finirebbe per avere una sovrabbondanza di codice.L'uso dell'approccio dichiarativo permette di eliminare questa ridondanza. Tuttavia, se non si desidera impiegare l'approccio dichiarativo o se non si pu perch si usa una versione di Struts che non lo supporta, esiste un metodo alternativo che non genera una cos grande ridondanza. Nel Capitolo 5, si visto come l'impiego di una classe di base Action astratta quale StorefrontBaseAction possa ridurre la ridondanza all'interno delle classi Action per altri motivi. Anche in questo caso si pu spingere la funzionalit della gestione programmatica delle eccezioni fino alla classe abstract cosi che non si debba farlo in tutte le proprie classi Action. A tal fine si dovr implementare il metodoexecuteAction(), introdotto dalla classe StorefrontBaseAction che un'implementazione del design pattern Template. L'Esempio 10-7 mostra il metodo execute() della classe StorefrontBaseAction.
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { ActionForward forwardPage = null; try{ UserContainer userContainer = getUserContainer( request ); // Inform the specific action instance to do its thing forwardPage = executeAction(mapping, form, request, response, userContainer); }catch (BaseException ex){ // Log the application exception using your logging framework // Call the generic exception handler routine forwardPage = processExceptions( request, mapping, ex ); }catch (Throwable ex){ // Log the system exception using your logging framework // Make the exception available to the system error page request.setAttribute( Action.EXCEPTION_KEY, ex ); // Treat all other exceptions as system errors forwardPage = mapping.findForward( IConstants.SYSTEM_FAILURE_KEY ); } return forwardPage; }
Il metodo execute() invoca il metodo executeAction() che deve essere ignorato da tutte le sottoclassi di Action e incapsula l'invocazione negli appositi blocchi try/catch. La classe StorefrontBaseAction astratta e offre una versione astratta del metodo executeAction() come mostrato di seguito:
abstract public ActionForward executeAction( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response, UserContainer userContainer ) throws BaseException;
Quando si verifica una qualsiasi eccef.idne dell'applicazione, essa sar raccolta nei blocchi try/catch all'interno del metodo exetute(), a patto che estenda la StorefrontBaseAction. Le sottoclassi non devono fornire un blocco catch a meno che non vogliano ulteriormente specializzare il comportamento di tale eccezione. Il metodo execute() passa l'eccezione insieme alla richiesta e agli oggetti del mapping al metodo processExceptions(), come illustrato dall'Esempio 10-8.
Esempio 10-8. The processExceptions( ) method in the StorefrontBaseAction
protected ActionForward processExceptions( HttpServletRequest request, ActionMapping mapping, BaseException ex ){ ActionErrors errors = new ActionErrors( ); ActionForward forward = null; // Get the locale for the user Locale locale = getUserContainer( request ).getLocale( ); if (locale == null){ // If it hasn't been conFigurad, get the default for the environment locale = Locale.getDefault( ); } processBaseException(errors, (FieldException) ex, locale);
// Either return to the input resource or a conFigurad failure forward String inputStr = mapping.getInput( ); String failureForward = mapping.findForward(IConstants.FAILURE_KEY); if ( inputStr != null) { forward = new ActionForward( inputStr ); }else if (failureForward != null){ forward = failureForward; } // See if this exception contains a list of subexceptions List exceptions = ex.getExceptions( ); if (exceptions != null && !exceptions.isEmpty( ) ){ int size = exceptions.size( ); Iterator iter = exceptions.iterator( ); while( iter.hasNext( ) ){ // All subexceptions must be BaseExceptions BaseException subException = (BaseException)iter.next( processBaseException(errors, subException, locale); } } // Tell the Struts framework to save the errors into the request saveErrors( request, errors ); // Return the ActionForward return forward; }
Il metodo processExceptions() sembra alquanto complesso ma non poi cos terribile. Ecco la sequenza di operazioni svolte al suo interno: 1. 2. 3. 4. 5. Ottiene le impostazioni locali per l'utente. Invoca il metodo processBaseException() per elaborare le eccezioni di livello alto. Se ci sono sottoeccezioni, le elabora una per una Salva tutti gli ActionError creati. Restituisce il controllo alla risorsa identificata nell'attributo input dell'azione ActionForward "Failure" che sia stata precedentemente configurata per l'azione.
);
oppure
una
Il metodo processBaseException() il luogo in cui sono creati gli oggetti ActionError: mostrato nellEsempio 10-9.
Esempio 10-9. Il metodo processBaseException() della classe StorefrontBaseAction
protected void processBaseException( ActionErrors errors, BaseException ex, Locale locale) { // Holds the reference to the ActionError to be added ActionError newActionError = null; // The errorCode is the key to the resource bundle String errorCode = ex.getMessageKey( ); /** * If there are extra arguments to be used by the MessageFormat object, * insert them into the argList. The arguments are context sensitive * arguments for the exception; there may be 0 or more. */ Object[] args = ex.getMessageArgs( ); /** * In an application that had to support I18N, you might want to * format each value in the argument array based on its type and the * user locale. For Esempio, if there is a Date object in the array, it
* would need to be formatted for each locale. */ // Now construct an instance of the ActionError class if ( args != null && args.length > 0 ){ // Use the arguments that were provided in the exception newActionError = new ActionError( errorCode, args ); }else{ newActionError = new ActionError( errorCode ); } errors.add( ActionErrors.GLOBAL_ERROR, newActionError ); }
II metodo processBaseException() ha la responsabilit di creare l'oggettoActionError. Fa uso del campo messageKey per ricercare un messaggio di tipo bundle e se sono inclusi degli argomenti, li include nel costruttore ActionError. Come si pu vedere, aggiungere alle proprie applicazioni la gestione programmatica delle eccezioni richiede molto pi lavoro rispetto all'uso del gestore del framework Struts. Diventa anche difficile la manutenzione se si compiono cambiamenti drastici nella gerarchia delle eccezioni o si cambia la modalit di gestione di determinate eccezioni. Tuttavia, se si usa una vecchia versione di Struts, questo approccio potrebbe costituire una scelta obbligata. Forse questi esempi andranno estesi appositamente per le proprie applicazioni ma l'approccio utilizzato gi ben progettato da permettere di utilizzarlo e inserirlo nelle proprie applicazioni. All'interno delle specifiche per EJB e servlet, la sicurezza programmatica non viene molto considerata perch troppo facile associare strettamente la propria applicazione all'ambiente di sicurezza fisica. Con la gestione delle eccezioni difficile che si debbano cambiare le eccezioni lanciate sulla base dell'ambiente di destinazione. Per questo motivo, non c' lo stesso rigore associato alla gestione programmatica delle eccezioni come vi per la sicurezza programmatica. Ma anche vero per che, se si pu trarre vantaggio dalla gestione delle eccezioni dichiarativa, la propria applicazione sar pi semplice da aggiornare rispetto al caso in cui le stesse funzionalit sono inglobate nel codice sorgente. Un'applicazione dovr essere sempre modificata nel corso del tempo e nuove eccezioni dovranno essere lanciate e catturate. Pi se ne specificano in maniera dichiarativa, pi facile sar la manutenzione.
Tuttavia i tag di Struts di solito conservano l'eccezione nello scope di request sotto la chiave:
Action.EXCEPTION_KEY
che fa riferimento a una stringa di org.apache.struts.action.Action.EXCEPTION_KEY. Se si necessita di avere accesso alla causa scatenante, si pu usare chiave per risalire all'oggetto. questa La versione 1.2 delle specifiche JSP ha modificato JSPException per il supporto della concatenazione di eccezioni, tuttavia gli sviluppatori di Struts probabilmente preferiranno lasciare i tag attualmente presenti per compatibilit con le versioni precedenti e trarranno vantaggio da questa nuova funzionalit solo per i futuri tag. Tuttavia, nei tag personalizzati che si creano, si dovrebbe sempre usare il campo rootCause nella classe JSPException quando si rilanciano eccezioni come tipi diversi.
// Detect some problem and throw an exception throw new InvalidLoginException( "An exception has occurred." );
Il problema insito nell'inserimento di una stringa nel codice sta nel fatto che utile solo per gli sviluppatori della stessa area di localizzazione. Potrebbe essere difficile per gli sviluppatori o per gli amministratori eli sistema di aree diverse l'uso dei file di log dove sono registrate queste eccezioni. Al posto di inserire stringhe nel codice direttamente potrebbe essere una scelta migliore prendere il messaggio dal resource bundle. Ovviamente non sono sotto il proprio controllo le eccezioni lanciate da package di terze parti, come pure sono difficili da individuare le immagini della "pila". Molte organizzazioni non si curano di localizzare i messaggi di eccezioni, cosa che va anche bene fino a quando nessuno di un'area locale diversa dalla propria deve cercare di interpretarle.
10.6 Conclusione
Il nuovo meccanismo di gestione dichiarativa delle eccezioni una grande integrazione al framework Struts e sicuramente permetter un maggior risparmio di tempo per gli sviluppatori sia durante lo sviluppo iniziale che per l'aggiornamento. Quando possibile si dovrebbe fare uno sforzo anche considerevole per trarre vantaggio da questa caratteristica piuttosto che scrivere programmaticamente la propria gestione delle eccezioni. Ma se si desidera o si ha la necessit di personalizzare la gestione delle eccezioni all'interno del framework questa possibilit non affatto preclusa.
CAPITOLO 11
Il framework Validator
Struts permette che la validazione dell'input avvenga all'interno dell' ActionForm. Per svolgere correttamente il compito della validazione di dati passati a un'applicazione Struts, gli sviluppatori devono scrivere una logica di validazione speciale all'interno di ogni classe ActionForm, Anche se quest'approccio funziona, ha tuttavia alcune limitazioni piuttosto serie. Questo capitolo costituisce un'introduzione al framework Validator di David Winterfeldt, che stato creato appositamente per l'interazione con i componenti di Struts e per poter dare una risposta a queste limitazioni. Il Validator permette la configurazione dichiarativa di routine di validazione per un'applicazione basata us Struts senza bisogno di scrivere alcuna logica speciale di validazione. Il Validator diventato tanto popolare e diffuso tra chi sviluppa con Struts che stato aggiunto all'elenco dei progetti Jakarta e alla distribuzione principale di Struts.
Se occorre estendere le regole predefinite, si potrebbero mettere le proprie regole personalizzate in un apposito file XML separato per mantenerle distinte da quelle prestabilite. Questa operazione verr utile al momento di aggiornare la propria versione del framework Validator. Il file validator-rules_l_1.dtd descrive la sintassi del file validation-rules.xml. L'elemento centrale l'elemento formvalidation che richiede uno o pi elementi global.
<validator name="required" classname="org.apache.struts.util.StrutsValidator" method="validateRequired" methodParams="java.lang.Object, org.apache.commons.validator.ValidatorAction, org.apache.commons.validator.Field, org.apache.struts.action.ActionErrors, javax.servlet.http.HttpServletRequest" msg="errors.required"> </validator>
L'elemento validator permette anche l'uso di un sottoelemento javascript ma per amor di sintesi non verr qui riportato. Il supporto per JavaScript all'interno del Validator discusso pi avanti nel capitolo. L'elemento validator supporta sette attributi come qui elencato:
L'attributo name assegna un nome logico alla regola di validazione. Viene usato per effettuare il reference della regola ad altre regole all'interno di questo file e del file specifico per le applicazioni che sar esaminato pi avanti. Il nome deve essere univoco. Gli attributi classname e method definiscono classe e metodo che contengono la logica per la regola di validazione. Per esempio, come stato visto nel codice sopra, il metodo validateRequired() nella classe StrutsValidator sar invocato per la regola di validazionerequired. L'attributo methodParams un elenco di parmetri separati da virgola per il metodo definito nell'attributo method. L'attributo msg una chiave proveniente dal resource bundle. Il framework Validator usa questo valore per ricercare un messaggio nel resource bundle di Struts quando si verifica un errore. Come predefiniti; Validator impiega i seguenti valori:
errors.required={0} is required. errors.minlength={0} cannot be less than {1} characters. errors.maxlength={0} cannot be greater than {1} characters. errors.invalid={0} is invalid. errors.byte={0} must be a byte. errors.short={0} must be a short. errors.integer={0} must be an integer. errors.long={0} must be a long. errors.float={0} must be a float. errors.double={0} must be a double. errors.date={0} is not a date. errors.range={0} is not in the range {1} through {2}. errors.creditcard={0} is not a valid credit card number. errors.email={0} is an invalid email address
Si dovrebbero aggiungere questi valori al resource bundle della propria applicazione, o in alternativa cambiare i valori della chiave in validation-rules.xml se si desiderano usare messaggi alternativi. L'attributo depends viene usato per specificare altre regole di validazione che dovrebbero essere chiamate prima della regola che lo specifica. L'attributo depends illustrato nella regola di validazione minLength:
<validator name="minLength" classname="org.apache.struts.util.StrutsValidator" method="validateMinLength" methodParams="java.lang.Object, org.apache.commons.validator.ValidatorAction, org.apache.commons.validator.Field, org.apache.struts.action.ActionErrors, javax.servlet.http.HttpServletRequest" depends="required" msg="errors.minlength"> </validator>
Prima che la regola di validazione minLength sia chiamata, verr invocata la regola required. Si pu anche impostare una regola che dipenda da regole multiple separando le regole nell'attributo depends con una virgola: depends="required,integer" Se una regola che specificata nell'attributo depends non riesce a compiere la validazione, non verr invocata la regola successiva. Per esempio, nella regola di validazione minLength mostrata precedentemente, il metodo
validateMinLength() non sar invocato se la regola di validazione required non va a buon fine. Questo perch
non ha senso controllare la lunghezza di un valore se non presente alcun valore. L'attributo finale supportato dall'elemento validator l'attributo jsFunctionName. Questo attributo facoltativo permette di specificare il nome della funzione JavaScript. Come predefinito verr usato il nome dell'azione di Validator. Il framework Validator piuttosto generico. Contiene regole molto semplici e a grana grossa che possono essere impiegate per qualsiasi applicazione. Come si vedr in seguito proprio questa genericit che permette al Validator di essere impiegato in applicazioni non basate su Struts. La classe org.apache.commons.Validator.GenericValidator implementa le regole generiche come fossero una serie di metodi pubblici statici. La Tabella 11-1 elenca le regole di validazione disponibili nella classe GenericValidator.
Tabella 11-1. Regole di validazione nella classe GenericValidator
Descrizione
isBlankOrNull Controlla che il campo non contenga il valore null e che la lunghezza del campo sia maggiore d zero isByte isDate isDouble isEmail isFloat isInRange isInt isLong isShort matchRegexp maxLength minLength
Controlla che il valore possa essere convertito in un byte primitivo. Controlla che il campo sia una data valida. Controlla che il valore possa essere convertito in una primitiva a doppia precisione. Controlla che il campo sia un indirizzo email valido. Controlla che il valore possa essere convertito in una primitiva a virgola mobile. Controlla che il valore stia in un determinato intervallo. Controlla che il valore possa essere convertito in una primitiva int. Controlla che il valore possa essere convertito in una primitiva long. Controlla che il valore possa essere convertito in una primitiva short. Controlla se il valore uguale all'espressione regolare. Controlla se il valore minore o uguale al massimo. Controlla se il valore maggiore o uguale al minimo.
Dal momento che le regole di validazione in GenericValidator sono cos granulari, gli sviluppatori di Struts hanno aggiunto una classe di utilit chiamata org.apache.struts.util.StrutsValidator, che definisce un insieme di metodi ad alto livello associati a Struts ma che ne rendono pi semplice l'interazione con Struts. Sono elencati qui di seguito senza descrizione, comunque molto simile a quella dei metodi di Tabella 11-1:
validateByte validateCreditCard validateDate validateDouble validateEmail validateFloat validateInteger validateLong validateMask validateMinLength validateMaxLength validateRange validateRequired validateShort
La classe StrutsValidator contiene la logica di validazione concreta usata da Struts. Questa classe e i metodi appena citali sono configurati in maniera dichiarativa nel file validation-rules.xml. Quando uno di questi metodi viene invocato e la validazione non ha successo, viene creato automaticamente un ActionError e aggiunto all'oggetto ActionErrors. Questi errori sono conservati nella richiesta e resi disponibili ai componenti dello strato view.
Il secondo file di configurazione richiesto da Validator validation.xml. Questo file specifico per una certa applicazione;descrive infatti quali regole di validazione del file validation-rules.xml debbano essere adoperate da uno specifico ActionForm. Questo significa che sono configurate in modo dichiarativo: non si deve aggiungere codice alla classe ActionForm. La logica di validazione associata con una o pi classi ActionForm attraversoquesto file esterno. Il file validation.xml gestito mediante il validation_1_1.dtd. L'elemento pi esterno l'elemento form-validation che pu contenere due oggetti figli, global e formset. L'elementoglobal pu essere presente nessuna o pi volte, mentre l'elemento formset deve essere presente una o pi volte
<global> <constant> <constant-name>phone</constant-name> <constant-value>^\(?(\d{3})\)?[-| ]?(\d{3})[-| ]?(\d{4})$</constant-value> </constant> <constant> <constant-name>zip</constant-name> <constant-value>^\d{5}(-\d{4})?$</constant-value> </constant> </global> Questo frammento include due costanti, phone e zip, anche se possono esserne incluse quante si desidera. Queste costanti sono disponibili per gli elementi all'interno della sezione formset. Si possono anche riutilizzare all'interno della sezione formset semplicemente riferendosi a loro per nome.
Un esempio contribuir alla migliore comprensione di questo punto. L'Esempio 11-1 mostra un semplice file validation.xml.
Esempio 11-1. Un semplice file Validation.xml
<form-validation> <global> <constant> <constant-name>phone</constant-name> <constant-value>^\(?(\d{3})\)?[-| ]?(\d{3})[-| ]?(\d{4})$</constantvalue> </constant> </global> <formset> <form name="checkoutForm"> <field property="phone" depends="required,mask"> <arg0 key="registrationForm.firstname.displayname"/> <var> <var-name>mask</var-name> <var-value>${phone}</var-value> </var> </field> </form> </formset> </form-validation>
Nell'Esempio 11-1, la costante phone dichiarata nella sezione global viene impiegata nell'elemento var per contribuire a validare la propriet phone. L'elemento formset pu contenere due elementi figli constant e form. L'elemento constant ha lo stesso formato di quello nella sezione global. Pu essere presente nessuna o pi volte, mentre l'elemento form pu essere presente una o pi volte all'interno dell'elemento formset:
Se non si ha alcuna necessit di internazionalizzare le proprie routine di validazione e si desidera impiegare l'impostazione predefinita si possono lasciare senza modifiche questi attributi o eliminarli. La sezione "Validazione e internazionalizzazione" pi avanti nel capitolo affronter nei particolari tale argomento. L'elemento form definisce un insieme di campi da validare mentre name corrisponde all'identificatore che l'applicazione assegna al form. Nel framework Struts, questo l'attributo name dalla sezione form-beans. L'elemento form definisce una serie di campi da validare. Contiene un singolo attributo name, che dovrebbe essere uguale a quello di uno degli attributi name dalla sezione form-beans del file di configurazione di Struts. L'elemento form pu contenere uno o pi elementi field:
Attributo
Descrizione Il nome della propriet del JavaBean (oActionForm) da validare. Elenco delimitato da virgole di regole di validazione da applicare a questo campo. Perch field abbia successo, tutti i validatori devono essere soddi fatti. s Il JavaBean corrispondente a questo form potrebbe includere una propriet page. Solo i campi coni un valore dell'attributopage uguale o minore al valore della propriet page nel form JavaBean sono elaborati. Questa caratteristica risulta utile quando si lisa un approccio di tipo "wizard" per completare un form molto grande per assicurarsi che la pagina non sia saltata.
property depends
page
Il nome del metodo che restituir un array o una Collection impiegata per recuperare indexedListProperty la lista e poi effettuare un ciclo attraverso quest'ultima svolgendo le validazioni per questo campo.
Sia ValidatorActionForm che DynaValidatorActionForm coincidono con la mappatura delle azioni piuttosto che con il nome del form. Vale a dire che, invece di coincidere per il nome del form nell'attributo name dell'elemento form, si pu usare l'attributo path dell'elemento action. In questo modo si permette allo stesso form di essere riutilizzato per diversi mapping di azioni, dove ciascun mapping pu dipendere da certi campi per essere validato mentre gli altri possono essere ignorati. L'elemento field contiene diversi elementi figli:
<field property="phone" depends="required,mask"> <msg name="mask" key="phone.invalidformat"/> <arg0 key="registrationForm.firstname.displayname"/> <var> <var-name>mask</var-name> <var-value>${phone}</var-value> </var> </field>
L'elemento msg supporta tre attributi:
<!ATTLIST msg name CDATA #IMPLIED key CDATA #IMPLIED resource CDATA #IMPLIED >
L'attributo name specifica la regola con la quale dovrebbe essere usato msg. Il valore dovrebbe essere quello di una delle regole specificate nel file validation-rules.xml o nella sezione global. L'attributo key specifica una chiave del resource bundle che dovr essere aggiunta ad ActionError se la validazione fallisce. Se si desidera specificare un messaggio scritto, anzich utilizzare il resource bundle si pu impostare a false l'attributo resource. In questo caso l'attributo key preso come stringa di testo.
L'elemento field permette di includere fino a quattro elementi aggiuntivi. Questi elementi, cio arg0, argl, arg2 e arg3, sono impiegati per passare valori addizionali al messaggio sia tramite il resource bundle sia tramite gli elementi var o constant. L'elemento arg0 definisce il primo valore sostitutivo, argl il secondo e cosi via. Ogni elemento arg supporta tre attributi, name, key e resource, che sono omologhi degli attributi dell'elemento msg prima descritto. L'Esempio 11-1 include gli elementi per arg0 e argl:
<field property="phone" depends="required,mask,minLength"> <arg0 key="registrationForm.firstname.displayname"/> <arg1 name="minlength" key="${var:minLength}" resource="false"/> <var> <var-name>mask</var-name> <var-value>${phone}</var-value> </var> <var> <var-name>minLength</var-name> <var-value>5</var-value> </var> </field>
L'ultimo degli elementi figli di field l'elemento var, come si nota nell'Esempio 11-1 e nel frammento precedente. Questo elemento pu impostare parametri che un elemento field pu passare a una delle sue regole di validazione come un valore massimo e minimo in un intervallo di validazione. Questi parametri possono anche essere referenziati da uno degli elementi arg con una sintassi di shell: $(var:var-name}. Nell'Esempio 11-1, il valore che viene sostituito per la costante phone passato attraverso la maschera della regola di validazione per cui pu essere adoperato per controllare se il valore della propriet conforme alla maschera phone appropriata. L'elemento field pu avere zero o pi elementi var. Una volta che si sono configurati i due file di risorse XML per la propria applicazione essi vanno posti nella directory WEB-INF. Saranno quindi referenziati all'interno del file di configurazione di Struts come descritto nella sezione successiva.
Se si usano ActionForm dinamiche, si dovrebbe usare la branca della gerarchia che fa capo a DynaValidatorForm Se si usano invece ActionForm standard, si pu usare ValidatorForm o una delle sue sottoclassi. Che si usino ActionForm dinamiche o regolari, il Validator si configura allo stesso modo. Occorre solo assicurarsi che qualsiasi sottoclasse di ActionForm si scelga la sezione form-bean del file di configurazione di Struts sia configurata usando il nome completo di classe Java. Si veda "Configurare le applicazioni Struts" nel Capitolo 4 per dettagli maggiori. La prima decisione da prendere quando si deve scegliere una sottoclasse ActionForm appropriata : dinamica o standard? Si noti che in entrambe le sottoderivazioni della classe ActionForm, sia dinamica che standard, ci sono due versioni del form ValidatorForm. La classe genitore chiamata ValidatorForm, o DynaValidatorForm per la branca dinamica. Ciascuna comprende una sottoclasse che contiene il nome Action nel suo titolo. La sottoclasse di ValidatorForm ValidatorActionForm, mentre DynaValidatorActionForm la sottoclasse di DynaValidatorForm. Lo scopo delle due versioni diverse di permettere l'associazione della validazione con la definizione di form-bean oppure la definizione dell'azione. Le classi ValidatorActionForm e DynaValidatorActionForm passano in seguito l'attributo path dall'elemento action nel Validator e il Validator usa il nome dell'azione per cercare tra le regole di validazione. Se si adopera ValidatorForm o DynaValidatorForm, viene usato il nome di ActionForm per cercare la regola di validazione da usare. La sola ragione per usare una o l'altra classe un controllo pi fine sulle regole di validazione da usare. Per esempio, si supponga che una ActionForm contenga tre diverse regole di validazione ma solo due di esse debbano venir svolte per una particolare azione. Si potrebbero configurare le regole per svolgere solo il sottoinsieme di regole di validazione quando quella certa azione viene invocata. Altrimenti tutte le regole sarebbero invocate. In generale l'uso di ValidatorForm o di DynaValidatorForm dovrebbe essere sufficiente per i propri scopi Si prenda in esame un esempio pi completo dell'utilizzo del framework Validator Come gi successo nei capitoli precedenti si prender ad esempio l'applicazione Storefront per migliorare la comprensione del Validator. In particolare si esaminer il form HTML usato per catturare le informazioni di spedizione durante il checkout dell'applicazione Storefront come da Figura 1-2.
Per questo esempio si user un form dinamico e quindi utilizzeremo la classe DynaValidatorForm per raccogliere i dettagli dell'indirizzo di spedizione. Dal momento che il processo di checkout si protrarr per diverse pagine e si dovranno raccogliere dati per pi pagine, si dovr configurare il bean del form affinch possieda uno scope di sessione. Si raccoglieranno anche tutte le informazioni di checkout in una sola classe ActionForm. Invece di avere uno ShippingForm e un CreditCardForm, si creer un solo form, chiamato CheckoutForm che cattura tutte quante le informazioni. Nel nostro file di configurazione di Struts si dovr configurare il checkoutForm come indicato di seguito:
<form-bean name="checkoutForm" type="org.apache.struts.validator.DynaValidatorForm"> <form-property name="firstName" type="java.lang.String"/> <form-property name="lastName" type="java.lang.String"/> <form-property name="address" type="java.lang.String"/> <form-property name="city" type="java.lang.String"/> <form-property name="state" type="java.lang.String"/> <form-property name="postalCode" type="java.lang.String"/> <form-property name="country" type="java.lang.String"/> <form-property name="phone" type="java.lang.String"/> </form-bean>
L'attributo type specifica l'esatta sottoclasse ActionForm. Nelle prime versioni beta di Struts 1.1, la sezione form-bean richiedeva che si impostasse l'attributo dynamic come true quando si usavano ActionForm, dinamiche. Questo non pi necessario dal momento che il framework determiner se la classe specificata nell'attributo type discende dalla classe DynaActionForm.
Il passo successivo consiste nella modifica della logica di validazione specifica per l'applicazione. Tale modifica va effettuata nel file validation.xml. Va dichiarata una regola di validazione per ciascuna propriet nel form che si deve validare. In alcuni casi si dovranno specificare regole multiple. Nella Figura 11-2, per esempio, il campo phone deve essere inserito e deve rispettare un formato specifico. Queste sono due regole che devono essere entrambe rispettate, altrimenti l'invio del form non avr successo. Non verr mostrato tutto il file validation.xml dal momento che molto esteso e la maggior parte di questo ridondante. La sezione mostrata nell'Esempio 11-2 permetter di capire meglio la connessione dei vari elementi.
Esempio11-2. Un file validation.xml d'esempio per il form checkout
<formset> <constant> <constant-name>phone</constant-name> <constant-value>^\(?(\d{3})\)?[-| ]?(\d{3})[-| ]?(\d{4})$</constantvalue> </constant> <constant> <constant-name>zip</constant-name> <constant-value>^\d{5}(-\d{4})?$</constant-value> </constant> <form name="checkoutForm"> <field property="firstName" depends="required,mask"> <arg0 key="label.firstName"/> <var> <var-name>mask</var-name> <var-value>^[a-zA-Z]*$</var-value> </var> </field> <field property="postalCode" depends="required,mask"> <arg0 key="registrationForm.zip"/> <var> <var-name>mask</var-name> <var-value>${zip}</var-value> </var> </field> <field property="phone" depends="required,mask"> <arg0 key="registrationForm.phone"/> <var> <var-name>mask</var-name> <var-value>${phone}</var-value> </var> </field> </form> </formset> </form-validation>
Ora che per Storefront stato configurato tutto, si pu provare a lanciare l'esempio. Il vantaggio di usare un approccio dichiarativo rispetto a quello programmativo che, una volta che si configurato tutto, si pu partire. L'assenza di programmazione rende l'approccio dichiarativo molto pi semplice. Questo specialmente vero per il framework validator. Non c' codice da scrivere finch ci sono necessit speciali. Quando si invia la pagina con le informazioni sull'indirizzo di spedizione con certi campi non compilati, entrano in gioco le regole di validazione. Il risultato illustrato dalla Figura 11-3.
public static boolean validateXXX( java.lang.Object, org.apache.commons.validator.ValidatorAct ion, org.apache.commons.validator.Field, org.apache.struts.action.ActionErrors, javax.servlet.http.HttpServletRequest, javax.servlet.ServletContext );
dove validateXXX pu essere qualsiasi cosa, tranne un nome duplicato di regola. La Tabella 11-3 espone gli argomenti per il metodo validateXXX()
Parametro
Descrizione II JavaBean sul quale viene svolta la validazione. La ValidatorAction corrente. L'oggetto field che viene validato Gli oggetti errors da aggiungere ad ActionError se la validazione fallisce. L'oggetto di richiesta corrente II ServletContext dell'applicazione.
Nella maggior parte dei casi, il metodo dovrebbe essere statico. Tuttavia si possono anche definire metodi a livello di istanza. In ogni caso ci si deve assicurare che i propri metodi siano thread-safe. L'Esempio 11-3 illustra una nuova regola di validazione che valuta se un valore di tipo String sia un valore boolean valido.
Esempio 11-3. Una regola di validazione che valida un valore booleano
import import import import import import import import import import import
java.io.Serializable; java.util.Locale; javax.servlet.ServletContext; javax.servlet.http.HttpServletRequest; org.apache.commons.validator.Field; org.apache.commons.validator.GenericTypeValidator; org.apache.commons.validator.GenericValidator; org.apache.commons.validator.ValidatorAction; org.apache.commons.validator.ValidatorUtil; org.apache.struts.action.ActionErrors; org.apache.struts.util.StrutsValidatorUtil;
public class NewValidator implements Serializable { /** * A validate routine that ensures the value is either true or false. */ public static boolean validateBoolean( Object bean, ValidatorAction va, Field field, ActionErrors errors, HttpServletRequest request ) { String value = null; // The boolean value is stored as a String if (field.getProperty() != null && field.getProperty().length( ) > 0){ value = ValidatorUtil.getValueAsString(bean, field.getProperty( ) ); } Boolean result = Boolean.valueOf(value); if ( result == null ){ errors.add( field.getKey( ), StrutsValidatorUtil.getActionError(request, va, field)); } // Return true if the value was successfully converted, false otherwise return (errors.empty( )); } }
Il passo successivo consiste nell'aggiunta di questa nuova regola al file validation-rules.xml o, per mantenere le cose distinte, a un nuovo file. L'elemento validator per la regola validateBoolean dovrebbe avere questo aspetto:
msg="errors.boolean">
Infine si usi la nuova regola nel file validation.xml. Questo comprende anche la creazione di un elemento field che coincida con la propriet boolean su una ActionForm:
Nome tag
Descrizione Mostra tutti gli errori di validazione avvenuti durante l'elaborazione Determina se ci sono stati errori di validazione Mostra tutti i messaggi rinvenuti durante l'elaborazione Determina se ci sono stati messaggi durante l'elaborazione
I tag nella Tabella 11-4 consentono alle pagine JSP di riconoscere e di ottenere acceso a messaggi o errori riconosciuti nell'applicazione Struts. Questi tag sono stati gi trattati in dettaglio nel Capitolo 8.
<validator name="required" classname="org.apache.struts.util.StrutsValidator" method="validateRequired" methodParams="java.lang.Object, org.apache.commons.validator.ValidatorAction, org.apache.commons.validator.Field, org.apache.struts.action.ActionErrors, javax.servlet.http.HttpServletRequest" msg="errors.required"> <javascript><![CDATA[ function validateRequired(form) { var bValid = true; var focusField = null; var i = 0; var fields = new Array( ); oRequired = new required( ); for (x in oRequired) { if ((form[oRequired[x][0]].type == 'text' ||
form[oRequired[x][0]].type == 'textarea' || form[oRequired[x][0]].type == 'select-one' || form[oRequired[x][0]].type == 'radio' || form[oRequired[x][0]].type == 'password') && form[oRequired[x][0]].value == '') { if (i == 0) focusField = form[oRequired[x][0]]; fields[i++] = oRequired[x][1]; bValid = false; } } if (fields.length > 0) { focusField.focus( ); alert(fields.join('\n')); } return bValid; }]]> </javascript> </validator>
Quando il tag JavascriptValidator viene incluso nella pagina JSP, il testo dell'elemento javascript viene scritto nella pagina JSP per permettere alla pagina di offrire una validazione lato client. Quando l'utente invia il form, viene svolta la validazione lato client e per ogni validazione fallita sono mostrati messaggi all'utente. Si dovr includere il tag javascript con il nome dell'ActionForm che sta per validare:
<html:javascript formName="checkoutForm"/>
L'attributo formName viene impiegato per ricercare la serie di regole di validazione da impiegare come JavaScript nella pagina. Si dovr aggiungere manualmente un gestore degli eventi onsubmit per il form:
<html:form action="getPaymentInfo" onsubmit="return validateCheckoutForm(this);"> La funzione JavaScript validateCheckoutForm() viene invocata all'atto dell'invio del form. Verrano eseguite le regole di validazione e, se una o pi regole non trovano riscontro positivo, il form non sar inviato. Il tag javascript genera una funzione con il nome di validateXXX(), dove XXX il nome dell' ActionForm . Per esempio, se la propria ActionForm si chiama checkoutForm, il tag javascript creer una funzione JavaScript validateCheckoutForm() che esegue la logica di validazione. E questo il motivo per cui il gestore degli eventi onsubmit() chiama la funzione validateCheckoutForm().
Come impostazione predefinita, il tag JavascriptValidator genera funzioni JavaScript sia dinamiche che statiche. Se si desidera includere un file separato che contiene funzioni JavaScript statiche per trarre vantaggio dalla possibilit di caching del browser o per meglio organizzare la propria applicazione, si possono usare gli attributi dynamicJavascript e staticJavascript, entrambi con il flag impostato a true, come valore predefinito. Si pu impostare l'attributo staticJavascript a false nel proprio form e includere una pagina JavaScript separata con l'attributo dynamicJavascript impostato a false e l'attributo staticJavascript impostato a true. Si veda la documentazione del tag JavascriptValidator per ulteriore approfondimento..
<formset> <form name="registrationForm"> <field property="firstName" depends="required,mask,minLength"> <arg0 key="registrationForm.firstname.displayname"/> <var> <var-name>mask</var-name> <var-value>^\w+$</var-value> </var> <var> <var-name>minLength</var-name> <var-value>5</var-value> </var> </field> </form> </formset> <formset language="fr"> <form name="registrationForm"> <field property="firstName" depends="required,mask,minLength"> <arg0 key="registrationForm.firstname.displayname"/> <var> <var-name>mask</var-name> <var-value>^\w+$</var-value> </var> </field> </form> </formset>
protected ValidatorResources resources = null; private String pathnames = null; public ValidatorLoader( loadPathnames( ); initResources( ); } ) throws IOException {
public ValidatorResources getResources( return resources; } public String getPathnames( return pathnames; } ) {
){
public void setPathnames(String pathnames) { this.pathnames = pathnames; } protected void loadPathnames( ){ // Set a default just in case String paths = "validation-rules.xml,validation.xml"; InputStream stream = null; try{ // Load some properties file stream = this.getClass( ).getResourceAsStream( "validator.properties" ); if ( stream != null ){ Properties props = new Properties( ); props.load( stream ); // Get the pathnames string from the properties file paths = props.getProperty( "validator-pathnames" ); } }catch( IOException ex ){ ex.printStackTrace( ); } setPathnames( paths ); } protected void initResources( ) throws IOException { resources = new ValidatorResources( ); if (getPathnames() != null && getPathnames().length( ) > 0) { StringTokenizer st = new StringTokenizer(getPathnames( ), RESOURCE_DELIM); while (st.hasMoreTokens( )) { String validatorRules = st.nextToken( ); validatorRules = validatorRules.trim( ); InputStream input = null; BufferedInputStream bis = null; input = getClass( ).getResourceAsStream(validatorRules); if (input != null){ bis = new BufferedInputStream(input); try { // pass in false so resources aren't processed // until last file is loaded ValidatorResourcesInitializer.initialize(resources, bis, false); }catch (Exception ex){ ex.printStackTrace( ); } } } // process resources
resources.process( } } }
);
Il lavoro svolto in ValidatorLoader dell'Esempio 11-4 molto simile a quello di ValidatorPlugIn: carica e inizializza un'istanza della classeValidatorResources. L'oggetto una rappresentazione in memoria delle regole di validazione per un'applicazione. Questo esempio impiega il metodogetResourceAsStream() per trovare e caricare un file di propriet che contiene la lista dei file delle risorse del Validator. Una volta creata e inizializzata un'istanza della classe ValidatorResources, andr conservata in un qualche luogo. In un'applicazione Struts conservata in ServletContext: la nostra applicazione pu fare affidamento su questo oggetto o si pu inglobare la richiesta in un Singleton.
<global> <validator name="currency" classname="com.oreilly.struts.storefront.Validator" methodParams="java.lang.Object,org.apache.commons.validator.Field,java.uti l.List" method="isCurrency" msg="Value is not a valid Currency Amount."/> </global>
Una volta scritta la regola di validazione, essa va aggiunta al file di validazione specifico per l'applicazione:
<form-validation> <global> </global> <formset> <form name="checkoutForm"> <field property="paymentAmount" depends="required,currency"> <arg0 key="registrationForm.paymentamount.invalid"/> </field> </formset> </form-validation>
Da qualche parte nell'applicazione si deve aver accesso all'istanza dell'oggetto ValidatorResources inizializzato nell'Esempio 11-4 da usare per validarc il JavaBean:
ValidatorResources resources = // Get instance of the ValidatorResources Validator validator = new Validator(resources, "checkoutForm"); validator.addResource(Validator.BEAN_KEY, bean); validator.addResource("java.util.List", lErrors); try { // Execute the validation rules validator.validate( ); } catch ( ValidatorException ex ) { // Log the validation exception log.warn( "A validation exception occured", ex ); }
Anche se il framework Validator progettato per l'utilizzo indipendente da Struts, necessario un po' di lavoro prima di poterlo impiegare in questo secondo modo. Tuttavia vale la pena di spendere un po' di fatica per poi risparmiare un mucchio di tempo nel ciclo di sviluppo.
CAPITOLO 12
Internazionalizzazione e Struts
Le aziende non possono pi permettersi di pensare solo ai mercati della propria regione geolinguistica. A partire dalla seconda met degli anni Novanta, il mondo dell'economia stato tempestato di idee che riguardano un'economia di stampo mondiale: si pensi a quello che successo in Europa con l'introduzione della moneta unica. L'economia e le nazioni comprendono di non poter pi pensare solo ai loro mercati tradizionali se vogliono continuare a crescere in maniera significativa; occorre pensare in modo globale e tentare di vendere i propri prodotti e servizi a utenti di tale mondo globalizzato. Con l'esplosione del World Wide Web. alla met degli anni Novanta, le aziende che proponevano i loro prodotti su Internet iniziarono a pensare che offrire un accesso a prodotti e servizi tramite un sito web fosse un modo ideale di attrarre nuovi utenti da tutto il mondo. Una delle ragioni chiave sta nella possibilit di accedervi 24 ore su 24 e 7 giorni su 7. Il web permette a un consumatore di poter fare acquisti, senza le limitazioni dovute al tempo e alle fasce orarie, in qualsiasi momento del giorno o della notte. I tradizionali orari di esercizio sono irrilevanti sul Web. Il significato dell'accesso libero per le aziende di straordinaria importanza e per il supporto di clienti internazionali pu diventare davvero problematico per gli sviluppatori che devono scrivere e aggiornare applicazioni. Questo capitolo si focalizza su ci che serve per rendere le applicazioni basate su Struts adatte per qualsiasi utente mondiale senza limitazioni di lingua o di area geografica. Come spesso si verifica nel caso dello sviluppo di software, la pianificazione l'aspetto pi importante per assicurarsi il successo. Dalla lettura di questo capitolo si potr comprendere come strutturare applicazioni basate su Struts che rendono possibile il supporto di un vasto bacino di utenza.
sar veramente il pubblico cui ci si riferisce. Tuttavia, se vi capitalo mai di reingegnerizzare un'applicazione perch si cambiato parere riguardo alle cose date per scontate, saprete quanto possa essere difficile tornare indietro e correggere il progetto una volta realizzata l'applicazione. L'internazionalizzazione (I18N, sigla composta dalla prima e dall'ultima lettera della parola InternationalizatioN, entro le quali e riportato il numero di lettere comprese tra I e N) , in parole povere il processo di progettazione del proprio software in previsione del supporto linguistico per pi aree onde evitare di tornare a riscrivere l'applicazione tutte le volte che devono essere supportate nazioni e/o lingue nuove. Per dire che un'applicazione supporta l'internazionalizzazione, essa deve avere le seguenti caratteristiche: Possono essere supportati linguaggi aggiuntivi senza necessit di cambiamenti a livello del codice. Elementi di testo, messaggi e immagini hanno una collocazione esterna al codice sorgente. Dati dipendenti dalla cultura locale come date e orari, valori decimali e valuta sono correttamente formattati a seconda della collocazione geolinguistica dell'utente. Sono supportati set di caratteri non standard. L'applicazione pu essere adattata rapidamente a nuove lingue e/o regioni. Quando si internazionalizza un'applicazione, non ci si pu permettere di scegliere quali opzioni devono essere supportate. Vanno implementate tutte, altrimenti il processo si interrompe. Se un utente visita un sito web e tutti i testi, le immagini e i bottoni sono nella lingua corretta ma numeri e valute non sono visualizzati propriamente, l'utente non potr essere a proprio agio. Assicurarsi che l'applicazione possa supportare lingue e aree multiple solo un primo passo. Si deve anche creare una versione localizzata dell'applicazione per ciascuna area geolinguistica che si vuole supportare. Per fortuna i vantaggi di Java e delI'I18N vengono in aiuto proprio per questo. Per le applicazioni che siano state internazionalizzate in modo corretto tutto il lavoro da fare per supportare una nuova lingua o un nuovo Paese rimane esterno al codice sorgente. Si dice locale una regione (di solito geografica ma non necessariamente) che condivide costumi, cultura ed espressione linguistica. Le applicazioni scritte per un solo locale sono di solito definite miopi. La localizzazione (L10N) il processo mediante il quale si adatta la propria applicazione, che stata precedentemente internazionalizzata, alle impostazioni specifiche di un locale. Per applicazioni in cui il supporto per l'I18N non stato pianificato o reso disponibile, di solito questa operazione implica cambiamenti nel codice in cui sono incorporati testo, immagini e messaggi. Dopo che si sono applicati i cambiamenti, il codice sorgente deve essere ricompilato. Si immagini cosa diventerebbe un simile processo ripetuto per tutte le nuove impostazioni locali da supportare! A sentire Richard Gillam dell'Unicode Technology Group, che ha progettato la maggior parte del supporto per l'I18N nelle librerie Java, "L'internazionalizzazione non una caratteristica facoltativa". Gli utenti si aspettano che i prodotti funzionino per loro nelle loro lingue. Quando quello che si d per scontato non corretto, le cose vanno male e gli utenti potrebbero anche rimanere infastiditi. Bisogna iniziare a progettare con un certo anticipo il supporto per l'I18N nella propria applicazione. Anche se sembra che non se ne avr mai bisogno, implementandola comunque si sar sempre un passo avanti e ci non ritarder lo sviluppo della propria applicazione. Non tutte le applicazioni necessitano del supporto di I18N e alcuni programmatori o organizzazioni ,di sviluppatori aggrottano le sopracciglia se sentono parlare di aggiunta di funzionalit al di fuori dei requisiti. Tuttavia, anche se la propria applicazione non deve supportare pi di una impostazione di localizzazione, ci sono altri benefici derivanti dall'inclusione di alcuni aspetti della I18N. Per esempio, usando il resource bundle per tutto il testo statico, s pu risparmiare tempo sia per lo sviluppo che, pi importante ancora, per l'aggiornamento. Nel corso del capitolo si vedr come tutto ci sia vero.
La classe Locale dota Java delle istanze del concetto di locale poco sopra illustrato. Una particolare istanza di Locale rappresenta un'unica lingua e regione. Quando una classe all'interno della libreria Java modifica la sua funzionalit mentre svolge il suo compito sulla base di un oggetto Locale si dice che locale-sensitive. Per esempio, java.text.DateFormat locale-sensitive dal momento che formatter una data a seconda di un oggetto Locale particolare. Gli oggetti Locale non svolgono alcun lavoro di interpretazione o formattazione del tipo I18N, ma sono usati come identificatori dalle classi locale-sensitive. Quando si ottiene un'istanza della classe DateFormat possibile passarla in un'oggetto Locale per gli Stati Uniti. la classe DateFormat a compiere tutte le operazioni locale-sensitive di interpretazione e formattazione, basandosi sulla Locale solamente per identificare il formato adatto. Occorre prestare molta attenzione quando si impiega la classe java.text.Format o una qualsiasi classe da questa discendente, incluse DateFormat, NumberFormat e SimpleDateFormat, dal momento che non sono thread-safe. Il problema della sicurezza dei thread sussiste dal momento che un'istanza della classe Calendar viene conservata come variabile e si accede ad essa durante le invocazioni dei metodi parse()e format(). Si dovr impiegare un'istanza separata per ciascun thread o sincronizzare esternamente l'accesso. Non si deve conservare un'istanza singola ad esempio nello scope dell'applicazione e permettere a thread multipli di accedervi. Tuttavia possibile conservare le istanze all'interno della sessione utente usando differenti istanze per ciascun utente. Il problema della sicurezza dei thread coinvolge tutte le versioni di Java, inclusa la 1.4. La documentazione delle API per le classi Format stata aggiornata includendo la descrizione di questo problema di progettazione. Quando si crea un oggetto Locale, di solito si specifica la lingua e il codice della nazione. Il seguente frammento di codice illustra la creazione di due oggetti Locale uno per gli Stati Uniti e l'altro per la Gran Bretagna:
Locale usLocale = new Locale("en", "US"); Locale gbLocale = new Locale("en", "GB");
Il primo argomento nel costruttore il codice della lingua. Il codice della lingua consta di due lettere minuscole e deve essere conforme alle specifiche ISO-6.39. Si pu trovare un elenco completo dei codici linguistici all'URL http://www.unicode.org/unicode/onlinedat/languages.html. Il secondo argomento il codice nazionale. Consiste di due lettere maiuscole che devono essere conformi alle specifiche ISO-3166. La lista dei codici nazionali disponibile all'URL http://www.unicode.org/unicode/onlinedat/countries.html. La classe Locale offre molte costanti statiche che permettono di ottenere un'istanza delle impostazioni locali pi diffuse. Per esempio per ottenere un'istanza delle impostazioni locali giapponesi se ne pu impiegare una tra le due seguenti:
);
Il web container normalmente impiegher le impostazioni predefinite di locale per il proprio ambiente ma user un locale in base alla richiesta del client in HttpServletRequest per visualizzare a uso dell'utente finale informazioni localizzate.
public java.util.Locale getLocale( ); public java.util.Enumeration getLocales( ); Entrambi questi metodi impiegano l'header Accept-Language che parte costituente di ciascuna richiesta client
inviata al container delle servlet.
Dal momento che il web server non mantiene aperta a lungo la comunicazione con il browser, le preferenze locali del client sono inviate al servlet container unitamente a ogni richiesta. Struts, come comportamento predefinito, conserver queste informazioni nella sessione dell'utente permettendo di inviarle una sola volta. L'oggetto Locale, se conservato all'interno della sessione, vi conservato con una chiave Action.LOCALE_KEY, che lo trasmette alla stringa org.apache.struts.action.LOCALE. La configurazione della possibilit di conservare le impostazioni locali dell'utente nella sessione avviene nel file di configurazione di Struts, impostando l'attributo locale nell'elemento controller. Se non si imposta alcun valore, questo sar automaticamente impostato a false. Si veda il Capitolo 4 per maggiori informazioni sulle impostazioni del locale. L'invocazione del metodo getLocale() sull'oggetto HttpServletRequest restituisce le impostazioni locali del client mentre il metodo; getLocales() restituisce un elenco (Enumeration) delle localizzazioni preferite in ordine decrescente. Se il client non ha configurato le preferenze di localizzazione, il container delle servlet impiegher le proprie impostazioni locali predefinite. L'Esempio 12-1 illustra come determinare questa informazione con l'impiego di una servlet.
Esempio 12-1. Determinazione delle impostazioni locali dell'utente in una servlet
/** * Prints out information about a user's preferred locales */ public class LocaleServlet extends HttpServlet { private static final String CONTENT_TYPE = "text/html"; /** * Initialize the servlet */ public void init(ServletConfig config) throws ServletException { super.init(config); } /** * Process the HTTP Get request */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(CONTENT_TYPE); PrintWriter out = response.getWriter( ); out.println("<html>"); out.println("<head><title>The Esempio Locale Servlet</title></head>"); out.println("<body>"); // Retrieve and print out the user's preferred locale Locale preferredLocale = request.getLocale( ); out.println("<p>The user's preffered Locale is " + preferredLocale + "</p>"); // Retrieve all of the supported locales of the user out.println("<p>A list of preferred Locales in descreasing order</p>"); Enumeration allUserSupportedLocales = request.getLocales( ); out.println("<ul>"); while( allUserSupportedLocales.hasMoreElements( ) ){
Locale supportedLocale = (Locale)allUserSupportedLocales.nextElement( ); StringBuffer buf = new StringBuffer( ); buf.append("<li>"); buf.append("Locale: "); buf.append( supportedLocale ); buf.append( " - " ); buf.append( supportedLocale.getDisplayName( buf.append("</li>"); // Print out the line for a single Locale out.println( buf.toString( ) ); } out.println("</ul>");
) );
// Get the container's default locale Locale servletContainerLocale = Locale.getDefault( ); out.println("<p>The container's Locale " + servletContainerLocale + "</p>"); out.println("</body></html>"); } }
Quando si eseguir la servlet dell'Esempio 12-1, si dovrebbe vedere un risultato simile a quello illustrato nella Figura 12-1.
L'output potrebbe essere anche diverso se si hanno impostazioni locali differenti nel proprio sistema. La maggior parte dei browser web permette di configurare le impostazioni di localizzazione che si desidera supportare. Con Internet Explorer, per esempio si possono modificare le impostazioni linguistiche nel menu a tendina Tools Internet Options. L'impostazione della localizzazione per l'utente facile con Struts. Ci sono di fatto parecchi modi di recuperare la classe Locale per l'utente a seconda di dove si desidera accedere. Se si opera all'interno di una classe Action, per esempio, si pu semplicemente chiamare il metodo getLocale() definito nella classe di base di Struts Action. Il seguente frammento mostra il metodo getLocale():
protected Locale getLocale(HttpServletRequest request) { HttpSession session = request.getSession( ); Locale locale = (Locale) session.getAttribute(LOCALE_KEY); if (locale == null) locale = defaultLocale; return (locale); } Si dovr passare l'oggetto request a questo metodo perch avr bisogno di usare HttpSession per ottenere le
impostazioni locali. Il metodo getLocale() restituir sempre un'istanza di Locale, anche se non ne conservata nessuna nella sessione utente. Il metodo restituir la propriet defaultLocale se necessario. La propriet defaultLocale conservata come una variabile membro statica cui ha accesso ogni sottoclasse di Action:
);
L'ottenimento delle impostazioni locali dell'utente da qualsiasi altra parte ugualmente diretto. Si possono recuperare direttamente dalla sessione come fa il metodo getLocale() impiegando Action.LOCALE_KEY:
Locale userLocale = (Locale)session.getAttribute( Action.LOCALE_KEY); // With this approach, always check the Locale to see if it's null if ( userLocale != null ){ // Access the Locale }
Dal momento che possibile che non sia conservata alcuna informazione riguardo a Locale nella sessione utente, si dovrebbe confrontare con null la propriet Locale prima di tentare di usarla. Se la propria applicazione permette all'utente di cambiare le impostazioni locali al volo potrebbe essere necessario chiamare il metodo getLocale() per ciascuna nuova request per vedere se l'utente ha cambiato queste impostazioni. Un esempio di questa operazione stato delineato nella classe CustomRequestProcessor dell'Esempio 5-4.
Si vedr un esempio della creazione di un resource bundle per un'applicazione Struts pi avanti nel capitolo, sotto "Il resource bundle di Struts"
error.requiredfield.name=The Name field is required to save. error.requiredfield.phone=The Phone field is required to save. // other resource messages
Quest'approccio funziona bene, ma cosa succederebbe se ci fossero centinaia di campi da riempire? Ci sar allora bisogno di un messaggio delle risorse per ciascun campo richiesto e il resource bundle potrebbe diventare smisurato e difficile da gestire. Si noti tuttavia che l'unica differenza tra i due messaggi il nome del campo richiesto. Un metodo pi facile e gestibile quello di usare la funzionalit della classe java.text.MessageFormat. Questo permette di operare nel modo seguente:
Esempio 12-2. Uso della classe MessageFormat per formattare messaggi con variabili testuali
import java.util.ResourceBundle; import java.util.Locale; import java.text.MessageFormat; public class FormatEsempio { public static void main(String[] args) { // Load the resource bundle ResourceBundle bundle = ResourceBundle.getBundle( "ApplicationResources" ); // Get the message template String requiredFieldMessage = bundle.getString( "error.requiredfield" ); // Create a String array of size one to hold the arguments String[] messageArgs = new String[1]; // Get the "Name" field from the bundle and load it in as an argument messageArgs[0] = bundle.getString( "label.name" ); // Format the message using the message and the arguments String formattedNameMessage = MessageFormat.format( requiredFieldMessage, messageArgs ); System.out.println( formattedNameMessage ); // Get the "Phone" field from the bundle and load it in as an argument messageArgs[0] = bundle.getString( "label.phone" ); // Format the message using the message and the arguments String formattedPhoneMessage = MessageFormat.format( requiredFieldMessage, messageArgs ); System.out.println( formattedPhoneMessage ); } }
I messaggi che contengono dati variabili sono conosciuti come messaggi compositi (compound messages). L'utilizzo di messaggi compositi permette di sostituire i dati specifici dell'applicazione con messaggi da resource bundle durante l'esecuzione. Pu anche ridurre il numero di messaggi che la propria applicazione richiede nel resource bundle e ci pu ridurre i tempi di traduzione in altre impostazioni di localizzazione.
L'utilizzo di messaggi compositi rende la traduzione appena pi complessa dal momento che il testo contiene valori che non saranno conosciuti se non al momento dell'esecuzione. Inoltre il personale che si occupa della traduzione deve tenere conto di dove va posta la variabile di testo nel messaggio localizzato dal momento che i valori di sostituzione potrebbero trovarsi in posizioni diverse nel messaggio per lngue diverse. Struts include le capacit della classe MessageFormat ma incapsula la funzionalit dietro i componenti all'interno del framework.
key=value
L'Esempio 12-3 mostra un file di propriet chiamato StorefrontMessageResources. properties che pu essere caricato da Struts.
Esempio 12-3. Un semplice resource bundle per Struts
global.title=Virtual Shopping with Struts global.error.invalidlogin=The login was unsuccessful! Please try again. global.required={0} is a required value. label.featuredproducts=This Weeks Featured Products label.email=Email Address label.password=Password label.returning label.firstName=First Name label.lastName=Last Name label.address=Address label.city=City label.state=State label.postalCode=Postal Code
label.zip=Zip label.country=Country label.phone=Phone button.add=Add Me button.delete=Delete button.checkout=Checkout button.saveorder=Save Order errors.required={0} is required. errors.minlength={0} can not be less than {1} characters. errors.maxlength={0} can not be greater than {1} characters. errors.invalid={0} is invalid. errors.byte={0} must be a byte. errors.short={0} must be a short. errors.integer={0} must be an integer. errors.long={0} must be a long. errors.float={0} must be a float. errors.double={0} must be a double. errors.date={0} is not a date. errors.range={0} is not in the range {1} through {2}. errors.creditcard={0} is not a valid credit card number. errors.email={0} is not a valid e-mail address.
Ci si deve assicurare di scrivere il nome del file con l'estensione .properties. altrimenti Struts non potr essere in grado di riconoscerlo. Si noti che le chiavi impiegate nell'Esempio 12-3 sono separate da un punto (.). Si potevano usare anche altri caratteri nelle chiavi o in alternativa una chiave a parola singola come labelPhone=Phone. L'utilizzo della spaziatura nelle proprie chiavi un ottimo modo per organizzare il testo localizzato per rendere poi facili gli aggiornamenti e le modifiche ed evitare le collisioni di nomi, in maniera simile a quanto accade con i nomi dei package per le classi Java. Si deve per fare attenzione quando si adoperano come separatori caratteri diversi dal punto. I due punti (:), per esempio, possono essere utilizzati al posto del segno uguale (=) per separare chiavi e valori, quindi causeranno problemi se inseriti all'interno delle proprie chiavi. Se non si vuole usare il punto nelle proprie chiavi si pu adottare l'uso del tratto sottosegnato o underscore (_) o del semplice trattino (-). Gli spazi dovrebbero essere evitati dal momento che possono essere fonte di problemi.
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request){ ActionErrors errors = new ActionErrors( );
if(getEmail() == null || getEmail().length( ) < 1) { errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("security.error.email.required")); } if(getPassword() == null || getPassword().length( ) < 1) { errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("security.error.password.required")); } return errors; }
L'argomento String passato nel costruttore della classe ActionError deve essere una chiave all'interno del resource bundle. Le classi ActionMessage e ActionError hanno parecchi costruttori, la maggior parte dei quali permette di passare degli argomenti di sostituzione per le occasioni in cui i propri messaggi sono messaggi compositi, come spiegato all'inizio di questo capitolo. Si possono anche creare ActionMessage e ActionError da una classe Action. Segue un piccolo esempio dalla classe Srorefront LoginAction:
);
String errorMsg = null; UserView userView = null; try{ // Attempt to authenticate the user userView = serviceImpl.authenticate(email, password); }catch( InvalidLoginException ex ){ ActionErrors errors = new ActionErrors( ); ActionError newError = new ActionError("error.security.invalidlogin"); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors );
return mapping.findForward( IConstants.FAILURE_KEY ); }catch( ExpiredPasswordException ex ){ ActionErrors errors = new ActionErrors( ); ActionError newError = new ActionError("error.security.passwordexpired"); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors ); return mapping.findForward( IConstants.FAILURE_KEY ); }catch (AccountLockedException ex){ ActionErrors errors = new ActionErrors( ); ActionError newError = new ActionError("error.security.accountlocked" ); errors.add( ActionErrors.GLOBAL_ERROR, newError ); saveErrors( request, errors ); return mapping.findForward( IConstants.FAILURE_KEY ); }
Questo frammento costituisce un'altra motivazione a usare la caratteristica di gestione dichiarativa delle eccezioni. Si noti la quantit di codice scritto: tutto poteva essere evitato con la semplice definizione all'interno del file di configurazione di Struts.
response.setContentType("text/html; charset=UTF-8");
Tuttavia si dovrebbe compiere questa operazione in molte posizioni differenti, rendendo l'aggiornamento e la personalizzazione pi difficoltosi.
Si potrebbe anche impostare ii browser perch invii gli URL sempre come UTF-8; di solito esiste un riquadro di selezione o un'opzione: in IE 5.5 nelle opzioni avanzate. Ci sono ancora due operazioni per assicurarsi che la propria applicazione supporti pienamente Unicode. La prima consiste nel garantire che il proprio database sia configurato per Unicode. Ci predefinito, ma sempre meglio fare una verifica. In secondo luogo, se si usa il JRE anzich SDK, si usi la versione I18N e non quella americana.
CAPITOLO 13
EJB: si o no?
Da quando l'architettura J2EE ha cominciato ad assumere grande importanza per lo sviluppo di applicazioni di livello enterprise, si spesso dato per scontato che se un'applicazione non includeva EJB non era una applicazione di tipo J2EE. Vi stata allora una gran corsa all'implementazione di sistemi di livello enterprise tramite i container EJB e tutto quello che potevano offrire. Per applicazioni che erano state progettate con cura e che richiedevano il tipo di infrastruttura inerente gli EJB, la tecnologia metteva a disposizione un approccio basato su standard, e ci ha condotto a molti successi. Purtroppo il peso dato alla porzione EJB di J2EE ha condotto anche all'uso di questi in applicazioni che sarebbero state meglio eseguite con mezzi pi leggeri. Tutto ci, insieme al fatto che alcuni sviluppatori non erano contenti della lentezza dell'evoluzione degli EJB, ha portato a una reazione di rifiuto totale degli EJB. Gli sviluppatori si sono schierati su uno dei due fronti di questa discussione e alcuni sviluppatori di Struts hanno fatto sentire la propria voce nella critica agli EJB. L'opinione pi moderata consiste nell'ammettere che gli EJB offrono un gran numero di servizi quando vi necessit d'elle loro potenzialit peculiari, ma possono anche essere "costosi", in termini sia di complessit che di tempo di sviluppo, qualora queste funzionalit non siano espressamente richiesto. Questo capitolo non prover neanche a valutare i pro e i contro degli EJB in quanto questo non rientra assolutamente negli obiettivi del libro. Si far conto invece che sia stato richiesto di scrivere uno strato web basato su Struts che deve interfacciarsi con un livello applicativo EJB. Partendo da questo presupposto, la preoccupazione principale consister nell'identificare i problemi chiave e trovare un approccio utile per risolverli.
un'interfaccia di servizio. In particolare si visto nel corso dell'esempio Storefront quanto possa essere facile effettuare lo scambio da un modello di debug a una implementazione completa che accede a un database, se si segue questo approccio di progettazione. Attraverso questo capitolo si user l'esempio di Storefront per illustrare come uno strato applicativo EJB possa essere usato con un'applicazione basata su Struts. Se non fosse nella natura originaria degli EJB la possibilit che una applicazione client faccia accesso ad essi, questa implementazione non avrebbe alcuna differenza rispetto all'altra. Tuttavia gli aspetti connessi all'architettura distribuita degli EJB devono essere tenuti presenti quando il proprio client dello strato web accede a questo tipo di model. Nel resto del capitolo si troveranno diverse raccomandazioni e suggerimenti riguardanti la scrittura di codice per l'interfacciamento con lo strato applicativo. Un approccio che isoli il codice che gestisce le caratteristiche peculiari degli EJB basilare al fine di non coinvolgere le proprie classi action.
diverrebbe rapidamente associato in modo stretto all'object model implementato dal livello applicativo. Questa stretta associazione, combinata con la natura distribuita degli EJB, porterebbe a un gran numero di problemi: I cambiamenti nell'object model EJB richiederebbero cambiamenti corrispondenti nello strato web. Le classi action dovrebbero spesso eseguire molteplici chiamate remote all'application server per soddisfare le necessit tli logica di elaborazione di una request. La logica di elaborazione e il codice per la gestione delle transazioni necessarie per manovrare chiamate multiple dovrebbero essere posti all'interno dello strato web. Per evitare questi problemi, l'interfaccia che viene esposta ai client del livello applicativo quasi sempre limitata ai session bean. Quest'approccio definito comunemente come "sessione che ingloba l'entity" o "session faade". Questo stile di progettazione offre alcuni vantaggi. Quando un'applicazione client rivolge una chiamata singola a un metodo di un session bean per svolgere le operazioni richieste, il session bean pu eseguire la richiesta come se fosse una singola transazione e i dettagli dell'implementazione possono essere nascosti. Questo metodo che usa i session bean potrebbe necessitare di un accesso ad altri entity bean, oppure anche ad altri session bean per poter soddisfare la request. Non ha importanza quale sia il flusso di controllo su un'application server, tanto la complessit viene comunque nascosta al client. Dal momento che i session bean divengono gli unici client degli entity bean, quando si usa una session faade ci sono poche probabilit che gli entity siano associati a qualsiasi tipo particolare di client esterno.
Anche se la discussione sta dando per scontato che i business object siano implementati come entity bean, questo non deve essere il caso in un'applicazione EJB. Gli stessi problemi e vantaggi che si ottengono tramite l'uso della session facade si incontrano anche con l'impiego di altre implementazioni. Proprio come alcuni sviluppatori Java non gradiscono gli EJB, non tutti gli sviluppatori di EJB amano gli entity bean. Dal momento che la session facade nasconde il modello dell'oggetto al client, gli entity bean possono essere sostituiti con altri approcci, come Java Data Objects (JDO) o normali data access objects (DAO), senza alcun impatto sull'interfaccia rivolta al client.
session bean. Potrebbe suonare strano ma non si vedr mai (o quasi mai) un metodo dichiarato esplicitamente in un'interfaccia remota. Ci a causa di un pattern di progettazione EJB detto business interface. Oltre ad avere un'interfaccia remota, un session bean che supporta client remoti deve avere un'interfaccia home che estenda javax.ejb.EJBHome, una classe di implementazione, e uno o pi descrittori di deploy XML. Diversamente dall'interfaccia remota che dichiara i metodi di elaborazione del bean. l'interfaccia home definisce metodi per creare riferimenti a session bean. Per definizione, i'metodi del bean sono implementati dalla classe di implementazione. I descrittori di deploy identificano e configurano un bean perch possa esseifc irnpiegato all'interno di un certo container EJB. Per ogni elemento verr definito un adeguato esempio nel capitolo.
Quando si considera una classe e un'interfaccia con cui associata, ci si aspetter che la classe implicitamente implementi quell'interfaccia. Questo non per vero per le interfacce locali o remote per quanto riguarda gli EJB. Al loro posto, il container crea un oggetto intermedio (cui spesso ci si riferisce come EJBObject) per implementare l'interfaccia remota. Quest'oggetto intercetta tutte le chiamate dei client del bean e in seguito le passa alla classe di implementazione dopo aver svolto tutte le operazioni richieste (ad esempio, gestione della sicurezza o delle transazioni). La responsabilit del controllo che la classe del bean implementi tutti i metodi business dichiarati dall'interfaccia remota passa dal compilatore Java agli strumenti del container per il deploy. Anche se la classe di un bean viene compilata senza problemi non se ne potr effettuare il deploy se ci sono discrepanze tra questa e la sua interfaccia remota. Se si dichiarato un session bean per implementare la sua interfaccia remota si pu stare sicuri che il compilatore riconosca qualsiasi problema con le sue dichiarazioni di metodi business. Il problema sorge dalla necessit di offrire implementazioni "inerti" dei metodi non-business dichiarati da javax.ejb.EJBObject. Questi metodi non dovrebbero essere mai invocati (sono invocati solo sull'oggetto intermedio creato dal client) ma devono essere presenti per poter compilare l'applicazione. Invece di quest'approccio, una gran parte di sviluppatori di EJB crea un'interfaccia secondaria, conosciuta con il nome di business interface, che dichiara i metodi di elaborazione che devono essere esposti. La dichiarazione dell'interfaccia remota per estendere quest'interfaccia e la classe bean che la implementa espone i metodi richiesti e il compilatore pu verificare che il bean li implementi. Questo modo di procedere offre un buon punto di partenza per la definizione del meccanismo di accesso dei client. L'uso di un'interfaccia business previene anche il passaggio accidentale per cui istituito un riferimento a this a un'istanza di una classe bean stata dichiarata come implementazione dell'interfaccia remota. Questo argomento non rientra nell'area di interesse di questo libro ma in breve si pu dire che un container EJB pu gestire in maniera appropriata le istanze solo quando ci si riferisce a queste utilizzando la loro interfaccia remota o locale. Il riferimento a un bean non pu essere restituito al posto della sua interfaccia remota se la classe del bean implementa solo l'interfaccia business. Ritornando all'interfaccia IStorefrontService che dovrebbe essere soddisfatta dall'implementazione che si deve eseguire, si ricordi che contiene metodi relativi sia all'autenticazione dell'utente sia al catalogo dei prodotti. Anche se si impiega una session facade, si desiderer probabilmente separare queste responsabilit in session bean separati. Questo riduce da un lato la complessit dei session bean coinvolti e dall'altro semplificher la manutenzione e l'aggiornamento. Tuttavia, dato che in questa sede non ci si vuole concentrare troppo sulla progettazione degli EJB, per semplificare si immaginer che la facade consista in un solo session bean Storefront. Probabilmente non si far la stessa cosa in un'applicazione reale, ma una volta appreso come interfacciali con un singolo session bean diventa immediato applicare la stessa tecnica a session bean multipli. Nell'Esempio 13-1 riportata un'interfaccia business adatta al session bean Storefront.
Esempio 13-1. L'interfaccia business del session bean per l'applicazione Storefront
package com.oreilly.struts.storefront.service; import import import import import java.rmi.RemoteException; java.util.List; com.oreilly.struts.storefront.catalog.view.ItemDetailView; com.oreilly.struts.storefront.customer.view.UserView; com.oreilly.struts.storefront.framework.exceptions.*;
public interface IStorefront { public UserView authenticate( String email, String password ) throws InvalidLoginException, ExpiredPasswordException, AccountLockedException, DatastoreException, RemoteException; public List getFeaturedItems( RemoteException; ) throws DatastoreException,
package com.oreilly.struts.storefront.service; import javax.ejb.EJBObject; public interface Storefront extends EJBObject, IStorefront { /** * The remote interface for the Storefront session bean. All methods are * declared in the IStorefront business interface. */ }
Se si parte con l'implementazione EJB che include gli entity bean, si potrebbe usare XDoclet (disponibile presso http://www.sourceforge.net/projects/xdoclet/) per generare in automatico ActionForms di Struts da questi bean. Per implementazioni EJB pi complesse rispetto a quella presa in esame, XDoclet dispone di mezzi automatici per la generazione di varie interfacce e descrittori di deploy richiesti da un bean. Questa generazione di codice viene svolta sulla base di tag JavaDoc speciali che si includono nelle proprie classi di implementazione degli EJB. Dal momento che non si impiegheranno gli entity bean, si potr fare uso dello stesso approccio ORM e delle stesse classi di entity business object gi impiegate nella classe StorefrontServiceImpl. Di fatto questa particolare nostra implementazione sembrer molto simile a quella classe, eccetto che per i metodi di callback richiesti dall'interfaccia javax.ejb.SessionBean. Si consideri quindi l'Esempio 13-3.
Esempio 13-3. Il session bean Storefront
package com.oreilly.struts.storefront.service; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.ejb.CreateException; import javax.ejb.EJBException; import javax.ejb.SessionBean; import javax.ejb.SessionContext; import org.odmg.*; import ojb.odmg.*; import com.oreilly.struts.storefront.businessobjects.CustomerBO; import com.oreilly.struts.storefront.businessobjects.ItemBO; import com.oreilly.struts.storefront.catalog.view.ItemDetailView; import com.oreilly.struts.storefront.catalog.view.ItemSummaryView; import com.oreilly.struts.storefront.customer.view.UserView; import com.oreilly.struts.storefront.framework.exceptions.AccountLockedException; import com.oreilly.struts.storefront.framework.exceptions.DatastoreException; import com.oreilly.struts.storefront.framework.exceptions.ExpiredPasswordException; import com.oreilly.struts.storefront.framework.exceptions.InvalidLoginException; /** * This is a simple Session Bean implementation of the Storefront service */ public class StorefrontBean implements SessionBean, IStorefront { private SessionContext ctx; private Implementation odmg = null; private Database db = null; public UserView authenticate( String email, String password ) throws InvalidLoginException, ExpiredPasswordException, AccountLockedException, DatastoreException { // Query the database for a user that matches the credentials List results = null; try{ OQLQuery query = odmg.newOQLQuery( ); // Set the OQL select statement String queryStr = "select customer from " + CustomerBO.class.getName( ); queryStr += " where email = $1 and password = $2"; query.create(queryStr); // Bind the input parameters query.bind( email ); query.bind( password );
// Retrieve the results results = (List)query.execute( ); }catch( Exception ex ){ ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } // If no results were found, must be an invalid login attempt if ( results.isEmpty( ) ){ throw new InvalidLoginException( ); } // Should only be a single customer that matches the parameters CustomerBO customer = (CustomerBO)results.get(0); // Make sure the account is not locked String accountStatusCode = customer.getAccountStatus( ); if ( accountStatusCode != null && accountStatusCode.equals( "L" ) ){ throw new AccountLockedException( ); } // Populate the value object from the Customer business object UserView userView = new UserView( ); userView.setId( customer.getId().toString( ) ); userView.setFirstName( customer.getFirstName( ) ); userView.setLastName( customer.getLastName( ) ); userView.setEmailAddress( customer.getEmail( ) ); userView.setCreditStatus( customer.getCreditStatus( ) ); return userView; } public List getFeaturedItems( ) throws DatastoreException { List results = null; try{ OQLQuery query = odmg.newOQLQuery( ); // Set the OQL select statement query.create( "select featuredItems from " + ItemBO.class.getName( ) ); results = (List)query.execute( ); }catch( Exception ex ){ ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } List items = new ArrayList( ); Iterator iter = results.iterator( ); while (iter.hasNext( )){ ItemBO itemBO = (ItemBO)iter.next( ); ItemSummaryView newView = new ItemSummaryView( ); newView.setId( itemBO.getId().toString( ) ); newView.setName( itemBO.getDisplayLabel( ) ); newView.setUnitPrice( itemBO.getBasePrice( ) ); newView.setSmallImageURL( itemBO.getSmallImageURL( ) ); newView.setProductFeature( itemBO.getFeature1( ) ); items.add( newView ); } return items; } public ItemDetailView getItemDetailView( String itemId ) throws DatastoreException { List results = null; try{ OQLQuery query = odmg.newOQLQuery( ); // Set the OQL select statement String queryStr = "select item from " + ItemBO.class.getName( queryStr += " where id = $1"; );
query.create(queryStr); query.bind(itemId); // Execute the query results = (List)query.execute( ); }catch( Exception ex ){ ex.printStackTrace( ); throw DatastoreException.datastoreError(ex); } // if (results.isEmpty( ) ){ throw DatastoreException.objectNotFound( } ItemBO itemBO = (ItemBO)results.get(0); // Build a ValueObject for the Item ItemDetailView view = new ItemDetailView( ); view.setId( itemBO.getId().toString( ) ); view.setDescription( itemBO.getDescription( ) ); view.setLargeImageURL( itemBO.getLargeImageURL( ) ); view.setName( itemBO.getDisplayLabel( ) ); view.setProductFeature( itemBO.getFeature1( ) ); view.setUnitPrice( itemBO.getBasePrice( ) ); view.setTimeCreated( new Timestamp(System.currentTimeMillis( view.setModelNumber( itemBO.getModelNumber( ) ); return view; } /** * Opens the database and prepares it for transactions. */ private void init( ) throws DatastoreException { // Get odmg facade instance odmg = OJB.getInstance( ); db = odmg.newDatabase( ); // Open database try{ db.open("repository.xml", Database.OPEN_READ_WRITE); }catch( ODMGException ex ){ throw DatastoreException.datastoreError(ex); } } public void ejbCreate( ) throws CreateException { try { init( ); }catch ( DatastoreException e ) { throw new CreateException(e.getMessage( )); } } public void ejbRemove( ) { try { if (db != null) { db.close( ); } }catch ( ODMGException e ) {} } public void setSessionContext( SessionContext assignedContext ) { ctx = assignedContext; } public void ejbActivate( ) { // Nothing to do for a stateless bean }
);
) ));
package com.oreilly.struts.storefront.service; import java.rmi.RemoteException; import javax.ejb.CreateException; import javax.ejb.EJBHome; /** * The home interface for the Storefront session bean. */ public interface StorefrontHome extends EJBHome { public Storefront create( ) throws CreateException, RemoteException; }
<?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 > <description>
Generic deployment information for the Storefront session bean </description> <display-name>Storefront Session Bean</display-name> <enterprise-beans> <session > <ejb-name>Storefront</ejb-name> <home>com.oreilly.struts.storefront.service.StorefrontHome</home> <remote>com.oreilly.struts.storefront.service.Storefront</remote> <ejbclass>com.oreilly.struts.storefront.service.StorefrontBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> <assembly-descriptor > <container-transaction > <method > <ejb-name>Storefront</ejb-name> <method-name>*</method-name> </method> <trans-attribute>NotSupported</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
In aggiunta al file ejb-jar.xml, la maggior parte dei container richiede uno o pi descrittori specifici per il produttore come integrazione delle informazioni di deploy di un bean. In questo caso ci che serve l'associazione di un nome JNDI con il bean Storefront. L'Esempio 13-6 mostra come operare con JBoss. Il nome completo dell'interfaccia remota del bean stato scelto come nome JNDI. Prassi comune usare il nome dell'interfaccia home.
Esempio 13-6. Il descrittore di deploy di JBoss per il session bean Storefront
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS//EN" "http://www.jboss.org/j2ee/dtd/jboss.dtd"> <jboss> <enterprise-beans> <session> <ejb-name>Storefront</ejb-name> <jndi-name>com.oreilly.struts.storefront.service.Storefront</jndiname> </session> </enterprise-beans> </jboss>
Il deploy di un EJB richiede di effettuare il package di questo in un file Java ARchive (JAR). Il file di deploy JAR per il nostro session bean deve includere i seguenti file: I file delle classi delle interfacce home e remote Il file della classe di implementazione del bean I due descrittori di deploy (questi file devono essere posti in una directory META-INF) Il file OJB.properties e i vari file XML usati dall'infrastruttura ORM I file delle classi di business object e DTO a cui viene effettuato il refererence dal bean Storefront Una volta creato questo file JAR, si pu effettuare il deploy all'interno della directory server/default/deploy nella propria installazione di JBoss. Si possono mettere invece in server/default/lib i file JAR per il proprio driver JDBC e le classi OJB. A questo punto si pu far partire JBoss e verificare che tutto sia a posto per eseguire l'applicazione.
package com.oreilly.struts.storefront.service; import import import import import import import import import import import java.rmi.RemoteException; java.util.HashTabella; java.util.List; javax.ejb.CreateException; javax.naming.Context; javax.naming.InitialContext; javax.naming.NamingException; javax.rmi.PorTabellaRemoteObject; com.oreilly.struts.storefront.catalog.view.ItemDetailView; com.oreilly.struts.storefront.customer.view.UserView; com.oreilly.struts.storefront.framework.exceptions.*;
/** * This class is a business delegate that supports the implementation of the * IStorefrontService interface using the Storefront session bean. */ public class StorefrontEJBDelegate implements IStorefrontService { private IStorefront storefront; public StorefrontEJBDelegate( init( ); } ) {
private void init( ) { try { HashTabella props = new HashTabella( ); props.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); props.put(Context.PROVIDER_URL, "localhost"); InitialContext ic = new InitialContext(props); Object home = ic.lookup("com.oreilly.struts.storefront.service.Storefront"); StorefrontHome sfHome = (StorefrontHome) PorTabellaRemoteObject.narrow(home, StorefrontHome.class); storefront = sfHome.create( ); } catch (NamingException e) { throw new RuntimeException(e.getMessage( } ));
catch (CreateException e) { throw new RuntimeException(e.getMessage( } catch (RemoteException e) { throw new RuntimeException(e.getMessage( } }
)); ));
public UserView authenticate( String email, String password ) throws InvalidLoginException, ExpiredPasswordException, AccountLockedException, DatastoreException { try { return storefront.authenticate(email, password); } catch (RemoteException e) { throw DatastoreException.datastoreError(e); } } public List getFeaturedItems( ) throws DatastoreException { try { return storefront.getFeaturedItems( ); } catch (RemoteException e) { throw DatastoreException.datastoreError(e); } } public ItemDetailView getItemDetailView( String itemId ) throws DatastoreException { try { return storefront.getItemDetailView(itemId); } catch (RemoteException e) { throw DatastoreException.datastoreError(e); } } public void logout( String email ) { // Do nothing for this Esempio } public void destroy( ) { // Do nothing for this Esempio } }
Quando viene creata un'istanza della classe StorefrontEJBDelegate, sono invocati il suo metodo init() per ottenere un reference remoto al session bean Storefront. Tale metodo opera le interrogazioni JNDI richieste usando il servizio di naming offerto da JBoss. Come gi detto, il BO delegate d per assunto che il servizio di naming sia in funzione sulla macchina locale. Pi avanti vedremo come rendere esterni i dettagli delle interrogazioni JNDI che devono essere svolte da un business object delegate. Ottenuto un reference remoto, il BO delegate lo trattiene come parte integrante del proprio stato. Dal momento va usato solo per l'accesso ai metodi di elaborazione, si dichiara questo campo come tipo dell'interfaccia business. Anche se il metodo storefront non viene dichiarato come tipo dell'interfaccia remota del session bean, il fatto che gestisca obbligatoriamente RemoteException rende evidente che il delegato sta accedendo a un oggetto remoto. Oltre a quello che richiesto per ottenere un reference remoto, la maggior parte del codice del business object delegate non fa altro che inviare chiamate dei metodi business all'implementazione del session bean. I metodi logout() e destroy() non hanno una controparte a livello applicativo, per cui tali implementazioni non includono chiamate ai session bean. Se ci fosse bisogno di una qualche operazione in questi metodi, quel codice dovrebbe essere implementato direttamente nei metodi di StorefrontEJBDelegate oppure in un'altro componente dello strato web che potrebbe essere chiamato dal BO delegate.
La gestione delle eccezioni che si trova in questa implementazione della classe StorefrontEJBDelegate merita particolare attenzione. Un business delegate usato con un session bean oltre a nascondere i dettagli delle interrogazioni JNDI dovrebbe anche nascondere le eccezioni specifiche per gli EJB non propagandole allo strato del client remoto. Nei business method del delegate, qualsiasi eccezione remota lanciata da una chiamata di sessione raccolta e al client viene mostrata solo una DatastoreException. In questo modo si pone rimedio al problema di non coerenza nelle eccezioni dichiarate tra l'interfaccia business e le dichiarazioni IStorefrontService. Se l'inclusione di RemoteException nella nostra interfaccia business stata sola differenza la rispetto a quella service, potrebbe nascere la tentazione di aggiungere quest'eccezione a IStorefrontService e proseguire avanti. Tuttavia questo potrebbe inutilmente compremettere il contratto per altre implementazioni di service.
L'unica ragione per cui il delegato usa una DatastoreException per rispondere a una RemoteException consiste nel lasciare l'interfaccia service non coinvolta dall'approccio implementativo. Se questa restrizione autoimposta fosse abbandonata e i cambiamenti a IStorefrontService fossero accettabili, un 'migliore approccio potrebbe essere quello di dichiarare una classe di eccezione il cui unico scopo sia quello di propagare eccezioni da un BO delegate in modo generico. Per esempio, se si dovesse dichiarare un'eccezione chiamata SetviceDelegateException, potrebbe essere lanciata quando capita una RemoteException. Invece di lanciare una RuntimeException per riportare un insuccesso circa l'ottenimento di un riferimento remoto, il metodo init() potrebbe essere anche caricato di questa ulteriore responsabilit e fare uso di ServiceDelegateException. Questa nuova eccezione darebbe una pi accurata indicazione del tipo di problema verificatosi rispetto a una DatastoreException. Inoltre l'aggiunta di questa nuova eccezione alle dichiarazioni di IStorefrontService continuerebbe a non rivelare che l'implementazione basata sugli EJB.
EJB home un oggetto di tipo factory valido per tutto il ciclo vitale dell'applicazione client. Non esiste uno stato in questo oggetto che impedisca di usarlo per tutte le richieste o i thread del client. Il delegate migliorerebbe in modo significativo se il reference home di cui necessita fosse salvato all'interno dello strato web dopo la prima richiesta. Come per ogni problema di progettazione c' pi di una tecnica da considerare per quest'operazione. Praticamente si sta parlando di dati che stanno nello scope dell'applicazione all'interno dello strato web, per cui una soluzione potrebbe essere quella di conservare i riferimenti all'interno del ServletContext dopo aver fatto le necessarie lookup JNDI. Questo renderebbe inutile qualsiasi interrogazione aggiuntiva ma richiederebbe di rendere il ServletContext disponibile al delegato attraverso il suo costruttore. Questo solo cambiamento si ripercuote sulla nostra service factory, dal momento che istanzia un'implementazione di IStorefrontService usando il suo costruttore senza argomento. Sarebbe meglio scegliere una soluzione senza uno stretto legame a costrutti HTTP. Un approccio pi flessibile per costituito dall'applicazione del pattern EJBHomeFactory come modo di conservare in una cache i reference di cui si necessita.
package com.oreilly.struts.storefront.framework.ejb; import import import import import import java.io.InputStream; java.io.IOException; java.util.*; javax.ejb.*; javax.naming.*; javax.rmi.PorTabellaRemoteObject;
/** * This class implements the EJBHomeFactory pattern. It performs JNDI * lookups to locate EJB homes and caches the results for subsequent calls. */ public class EJBHomeFactory { private Map homes; private static EJBHomeFactory singleton; private Context ctx; private EJBHomeFactory( ) throws NamingException { homes = Collections.synchronizedMap(new HashMap( )); try { // Load the properties file from the classpath root InputStream inputStream = getClass( ).getResourceAsStream( "/jndi.properties" ); if ( inputStream != null) { Properties jndiParams = new Properties( ); jndiParams.load( inputStream ); HashTabella props = new HashTabella( ); props.put(Context.INITIAL_CONTEXT_FACTORY, jndiParams.get(Context.INITIAL_CONTEXT_FACTORY)); props.put(Context.PROVIDER_URL, jndiParams.get(Context.PROVIDER_URL)); ctx = new InitialContext(props); } else { // Use default provider ctx = new InitialContext( ); } } catch( IOException ex ){ // Use default provider ctx = new InitialContext( );
} } /** * Get the Singleton instance of the class. */ public static EJBHomeFactory getInstance( ) throws NamingException { if (singleton == null) { singleton = new EJBHomeFactory( ); } return singleton; } /** * Specify the JNDI name and class for the desired home interface. */ public EJBHome lookupHome(String jndiName, Class homeClass) throws NamingException { EJBHome home = (EJBHome)homes.get(homeClass); if (home == null) { home = (EJBHome)PorTabellaRemoteObject.narrow(ctx.lookup( jndiName), homeClass); // Cache the home for repeated use homes.put(homeClass, home); } return home; } }
Il metodo getInstance di EJBHomeFactory si distingue da molte implementazioni Singleton per il fatto che non dichiarato come sincronizzato. Come gi discusso in Design Patterns, EJB l'uso di un metodo sincroniz degraderebbe le prestazioni zato senza alcun beneficio. Se si istanziano diverse istanze di EJBHomeFactory, date le chiamate simultanee getInstance nel corso dell'inizializzazione, potrebbero essere create alcun referece a a home ridondanti, anche se non ci saranno danni Si noter che la classe EJBHomeFactory accetta il nome e la classe JNDI per l'interfaccia home che deve localizzare. Se si deve accedere a pi di un session bean all'interno dello strato applicativo, si implementer semplicemente una classe delegata per ciascun session bean e si user la sua factory per cercare l'interfaccia home corrispondente. Oltre ai miglioramenti nelle prestazioni permesse dal caching delle home, tutti i "colli di bottiglia" e la gestione delle eccezioni che accompagnano la ricerca di questi reference vengono mantenuti in un'unica collocazione. Il costruttore per la EJBHomeFactory s occupa anche di esporre il provider e i parametri di factory di cui si necessita per accedere al servizio di naming. Per questa operazione si pu usare un approccio standard tramite file jndi.properties con le seguenti inclusioni:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.provider.url=localhost Se viene invocato un costruttore senza argomenti per la classe InitialContext, viene condotta una ricerca
attraverso il classpath per trovare un file jndi.properties Se viene tro vato, le inclusioni vengono impiegate per inizializzare il contesto di naming Nell'esempio, la classe factory carica esplicitamente il file dal classpath. Altrimenti potrebbe succedere che delle priorit di classloader possano anche impedire a questi valori di essere scelti con priorit rispetto ai valori predefiniti all'interno del server. L'esempio JNDIConnectorPlugin (nel Capitolo 9 ha dimostrato come i parametri del servizio di naming possano essere letti dal file web.xml e conservati all'interno di ServletContext. Qui stato evitato tale approccio allo scopo di mantenere distinti factory e strato web.
private void init( ) { try { StorefrontHome sfHome = (StorefrontHome)EJBHomeFactory.getInstance( lookupHome("com.oreilly.struts.storefront.service.Storefront", StorefrontHome.class); storefront = sfHome.create( ); } catch (NamingException e) { throw new RuntimeException(e.getMessage( )); } catch (CreateException e) { throw new RuntimeException(e.getMessage( )); } catch (RemoteException e) { throw new RuntimeException(e.getMessage( )); } }
).
package com.oreilly.struts.storefront.service;
import import import import import import import import import import
java.lang.reflect.*; java.rmi.RemoteException; java.util.*; javax.ejb.CreateException; javax.naming.*; javax.rmi.PorTabellaRemoteObject; com.oreilly.struts.storefront.catalog.view.ItemDetailView; com.oreilly.struts.storefront.customer.view.UserView; com.oreilly.struts.storefront.framework.ejb.EJBHomeFactory; com.oreilly.struts.storefront.framework.exceptions.*;
/** * This class is a dynamic proxy implementation of the IStorefrontService * interface. It implements two of the IStorefrontService methods itself and * delegates the others to the methods declared by the IStorefront business * interface with the same name. */ public class DynamicStorefrontEJBDelegate implements InvocationHandler { private IStorefront storefront; private Map storefrontMethodMap; public DynamicStorefrontEJBDelegate( init( ); } ) {
private void init( ) { try { // Get the remote reference to the session bean StorefrontHome sfHome = (StorefrontHome)EJBHomeFactory.getInstance( ). lookupHome("com.oreilly.struts.storefront.service.Storefront", StorefrontHome.class); storefront = sfHome.create( ); // Store the business interface methods for later lookups storefrontMethodMap = new HashMap( ); Method[] storefrontMethods = IStorefront.class.getMethods( ); for (int i=0; i<storefrontMethods.length; i++) { storefrontMethodMap.put(storefrontMethods[i].getName( ), storefrontMethods[i]); } } catch (NamingException e) { throw new RuntimeException(e.getMessage( } catch (CreateException e) { throw new RuntimeException(e.getMessage( } catch (RemoteException e) { throw new RuntimeException(e.getMessage( } } public void logout(String email) { // Do nothing for this Esempio } public void destroy( ) { // Do nothing for this Esempio } public Object invoke(Object proxy, Method method, Object[] args ) throws Throwable{ try { // Check for the two methods implemented by this class if (method.getName( ).equals("logout")) { logout((String)args[0]); )); )); ));
return null; } else if (method.getName( ).equals("destroy")) { destroy( ); return null; } else { // This method should match a method implemented by the // session bean that has the same name and argument list Method storefrontMethod = (Method)storefrontMethodMap.get( method.getName( )); if (storefrontMethod != null) { // Call the method on the remote interface return storefrontMethod.invoke( storefront, args ); } else { throw new NoSuchMethodException("The Storefront does not implement " + method.getName( } } } catch( InvocationTargetException ex ) { if (ex.getTargetException( ) instanceof RemoteException) { // RemoteException isn't declared by the IStorefront method that was // called, so we have to catch it and throw something that is throw DatastoreException.datastoreError(ex.getTargetException( )); } else { throw ex.getTargetException( ); } } } }
L'idea che sta dietro la creazione di DynamicStorefrontEJBDelegate di implementare un proxy dinamico creato come implementazione dell'interfaccia IStorefrontService per delegare tutte quelle invocazioni al metodo invoke() qui dichiarate. Si noti che questa classe delegata non viene dichiarata come se implementasse IStorefrontService. Di fatto i soli metodi di elaborazione da IStorefrontService che compaiono in questa classe sono il metodo logout() e il metodo destroy() che non sono implementati dal session bean. Per usare il BO delegate basato sul proxy dinamico si deve modificare la sua modalit per l'ottenimento dell'interfaccia service dalla factory. Invece di presentare un approccio elegante alla questione, si inseriranno direttamente nel codice le nuove modalit, come mostrato nella seguente versione del metodo createService() della StorefrontServiceFactory:
));
public IStorefrontService createService( ){ Class[] serviceInterface = new Class[] { IStorefrontService.class }; IStorefrontService proxy = (IStorefrontService)Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader( ), serviceInterface, new DynamicStorefrontEJBDelegate( ) ); return proxy; }
Quando una classe action richiede un'implementazione dell'interfaccia service, la factory crea un proxy dinamico che implementa quest'interfaccia impiegando un'istanza di DynamicStorefrontEJBDelegate. Quando l'action compie un'invocazione sull'interfaccia service la chiamata viene diretta al proxy che la trasforma in una chiamata al metodo del delegato invoke(). Il metodo invoke() controlla i nomi del metodo che stato chiamato e di conseguenza o chiama i metodi logout() oppure destroy() implementati nella classe o li delega a un session bean con lo stesso nome. Questa sequenza di chiamate illustrata dalla Figura 13-1. La cattura e la sostituzione di RemoteException sono ora gestite in un solo punto quando il business delegare chiama un metodo che un session bean. Il solo metodo invoke() pu gestire tutti i metodi esposti tramite l'interfaccia business dei session bean senza alcuna modifica.
Figura 13-1. Diagramma di sequenza sul recupero dei dettagli di un articolo in un proxy dinamico
Un proxy dinamico quindi una attraente tecnica per minimizzare la ridondanza dei metodi all'interno di un business delegate, ma si deve pagare un prezzo: dato che un approccio del genere si basa pesantemente sulla reflection, si deve tener conto della maggiore lentezza di esecuzione che risulter a fronte della migliorata gestibilit del codice.
13.3 Conclusioni
Non tutte le web application richiedono la sofisticazione e la complessit di un container EJB. I benefici pure ben documentati relativi all'impiego di un container EJB devono essere soppesati con seriet data l'accresciuta complessit sia dello sviluppo che della gestione. Non si deve dare per scontato di usare gli EJB per la porzione del model di un'applicazione Struts. Tuttavia, se si decide di usare gli EJB, si sappia che non avranno un impatto disastroso sul resto del codice. Esistono delle modalit da seguire per limitare l'accoppiamento tra la propria applicazione e gli EJB, le quali rendono il tutto molto pi facili da usare.
CAPITOLO 14
Usare Tiles
Fino a questo punto non si detto granch a proposito di organizzazione, montaggio di contenuti e aspetto delle pagine JSP per un'applicazione Struts; ci non rientra per vari motivi tra gli obiettivi di un libro su Struts ed esistono in commercio molti ottimi libri che offrono strategie per l'organizzazione di contenuti web e disposizione delle pagine. Nell'applicazione Storefront per la composizione delle pagine, si sono impiegati due approcci diversi. Il primo approccio, basato direttamente su JSP, probabilmente il pi familiare ai web designer. Le pagine JSP contengono logica di presentazione e tag di composizione HTML; non esiste una separazione tra i due elementi. Questo approccio impiegato di solito per web application piccole e non particolarmente complesse. Il secondo approccio fa uso della direttiva JSP include. Viene usato dagli sviluppatori per applicazioni web di grandi dimensioni o dopo aver compreso quanto possa essere ripetitivo l'approccio precedente. Se si speso molto tempo per la manutenzione delle web application, si sapr quanto possa essere noioso e difficile aggiornare l'aspetto di un sito. L'impiego della direttiva include delle JSP permette una maggiore riusabilit, riducendo i costi totali di sviluppo e manutenzione. Un terzo approccio, introdotto in questo capitolo, consiste nella riduzione della ridondanza del codice di una web application e, allo stesso tempo, nella separazione del contenuto dall'aspetto della pagina in modo pi netto rispetto agli altri due approcci.
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <html:html> <head> <title><bean:message key="global.title"/></title> <html:base/> <script language=javascript src="include/scripts.js"></script> <link rel="stylesheet" href="stylesheets/format_win_nav_main.css" type="text/css"> </head> <body topmargin="0" leftmargin="0" bgcolor="#FFFFFF">
<!-- Header information --> <%@ include file="include/head.inc"%> <!-- Menu bar --> <%@ include file="include/menubar.inc"%> <!--- Include the special offer --> <%@ include file="include/mainoffer.inc"%> <!-- Featured items header row --> <Tabella width="645" cellpadding="0" cellspacing="0" border="0"> <tr> <td width="21"> <html:img height="1" alt="" page="/images/spacer.gif" width="1" border="0"/> </td> <td width="534"> <html:img page="/images/week_picks.gif" altKey="label.featuredproducts"/> </td> <td width="1" bgcolor="#9E9EFF"> <html:img height="1" alt="" page="/images/spacer.gif" width="1" border="0"/> </td> <td width="1" bgcolor="#9E9EFF"> <html:img height="1" alt="" page="/images/spacer.gif" width="1" border="0"/> </td> <td width="90" bgcolor="#9E9EFF" align="right"> <html:img height="1" alt="" page="/images/spacer.gif" width="90" border="0"/> </td> </tr> <tr> <td> <html:img height="1" alt="" page="/images/spacer.gif" width="21" border="0"/> </td> <td colspan="4" bgcolor="#9E9EFF"> <html:img height="1" alt="" page="/images/spacer.gif" width="1" border="0"/> </td> </tr> </Tabella> <html:img height="10" alt="" page="/images/spacer.gif" width="1" border="0"/><br> <!--- Include the featured items --> <%@ include file="include/featureditems.inc"%> <!--- Include the copyright statement --> <%@ include file="include/copyright.inc"%> </body> </html:html>
Anche se nella pagina principale viene usata la direttiva JSP include, nella pagina gli aspetti relativi al layout sono frammisti ai contenuti. Per esempio, si noti come questa specifichi esplicitamente che il file di inclusione head.inc viene per primo, poi il file menubar.inc, poi il file mainoffer.inc e cos via fino al file copyright.inc a fondo pagina. Si devono porre le stesse dichiarazioni nello stesso ordine per ciascuna pagina che deve conservare questo particolare aspetto. Se un committente volesse far spostare il menu sul lato sinistro anzich destro, tutte le pagine andrebbero modificate di conseguenza. L'applicazione Storefront applica il meccanismo delle JSP include anzich basarsi sulle sole JSP. Anche se l'uso del meccanismo include un passo nella giusta direzione dal momento che riduce la ridondanza (si pu immaginare cosa succederebbe a dover includere dentro ogni pagina il contenuto del copyright), pur sempre meno efficiente di un approccio basato su template.
Con le JSP ci sono due diversi tipi di contenuto che possibile includere: statico e dinamico. La direttiva include di seguito mostrata:
include il sorgente della pagina cui si riferisce al momento della traduzione/compilazione. Perci, non possibile includere contenuti a runtime impiegando la direttiva include. Questa considera una risorsa come fosse un oggetto statico e il contesto della risorsa incluso letteralmente nella pagina. Al contrario, l'action include qui di seguito:
<jsp:include page="include/copyright.inc"/>
gestisce la risorsa come fosse un oggetto dinamico. La richiesta inviata alla risorsa e vi incluso il risultato dell'elaborazione. I template impiegano un approccio dinamico per cui alcune espressioni possono essere valutate e incluse a runtime.
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%> <%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles"%> <html:html> <head> <title><bean:message key="global.title"/></title> <html:base/> </head> <body topmargin="0" leftmargin="0" bgcolor="#FFFFFF"> <!-- Header page information --> <tiles:insert attribute="header"/> <!-- Menu bar --> <tiles:insert attribute="menubar"/> <!-- Main body information --> <tiles:insert attribute="body-content"/> <!-- Copyright information --> <tiles:insert attribute="copyright"/> </body> </html:html>
L'Esempio 14-2 non introduce nuovi concetti. La prima cosa da notare che si usano librerie di lag personalizzati di Struts. Il fatto che si stia usando la libreria di tag Tiles come pure la HTML e la Bean non dovrebbe risultare strano. La libreria Tiles una delle tante e verr discussa in dettaglio pi oltre nel capitolo. Il resto della pagina una commistione di tag di formattazione HTML. Si noter che non vi incluso alcun contenuto, ma solo tag insert dove deve essere inserito il contenuto a runtime. L'uso dei tag di Struts qui impiegati dovrebbe essere gi noto, per cui non se ne parler oltre. Il tag insert svolge un ruolo simile a quello della direttiva include. In buona sostanza questo tag indica che esiste una variabile header, per esempio, e che il valore dell'attributo "header" deve essere passato al tag insert e infine il contenuto generato deve essere posto in questo punto. Lo stesso dicasi per menubar, body-content, e copyright. Tra breve si spiegher come viene sostituito il contenuto "reale" al
posto di questi attributi durante l'esecuzione. Si noti che questo layout molto simile a quello descritto nell'Esempio 14-1. L'unica differenza che invece di includere esplicitamente gli include mainoffer e featureditem come da Esempio 14-1, il file del template include una sezione body-content. Questo permette di riusare il template per qualsiasi pagina che abbia questo formato generico. Una volta capito come inserire il contenuto del body specifico per la pagina, si pu riutilizzare questo template pi volte. Questo unico file pu allora controllare il layout di molte pagine. Se c' necessit di modificare il layout del sito, questo l'unico file da cambiare: ed il vero vantaggio di usare un approccio basato sui template. L'ultimo pezzo del puzzle consiste nel capire come sono assemblate le sezioni header, menubar, bodycontent e copyright per formare la risposta che viene visualizzata. Il punto importante da ricordare che la pagina JSP dell'Esempio 14-2 il template. C' sempre bisogno di pagine JSP che supportino contenuti specifici per la pagina usati dal template. Per esempio, se si riscrive index.jsp dell'Esempio 14-1 usando il template dell'Esempio 14-2, essa apparir come quella dell'Esempio 14-3 .
Esempio 14-3. La pagina index.jsp per l'applicazione Storefront facendo uso di un template
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %> <tiles:insert page="/layouts/storefrontDefaultLayout.jsp" flush="true"> <tiles:put name="header" value="/common/header.jsp" /> <tiles:put name="menubar" value="/common/menubar.jsp" /> <tiles:put name="body-content" value="/index-body.jsp" /> <tiles:put name="copyright" value="/common/copyright.jsp" /> </tiles:insert>
La prima cosa da notare nell'Esempio 14-3 che la libreria di tag Tiles viene inclusa all'inizio del file. Ogni pagina (o tile) che necessita di usare la libreria di tag Tiles deve includerla con questa riga:
<tiles:insert page="/layouts/storefrontDefaultLayout.jsp" flush="true"> <tiles:put name="header" value="/common/header.jsp" /> <tiles:put name="menubar" value="/common/menubar.jsp" /> <tiles:put name="body-content" value="/index-body.jsp" /> <tiles:put name="copyright" value="/common/copyright.jsp" /> </tiles:insert>
A runtime, i valori dei tag put sono sostituiti dinamicamente nel file template e quindi elaborati. Il risultato quello che poi viene mostrato al client. Per riassumere la discussione sui tempiale, ecco un'altra pagina che impiega lo stesso template dell'Esempio 14-2 ma che fornisce un diverso body-content. L'Esempio 14-4 mostra la pagina itemdetail.jsp.
Esempio 14-4. La pagina itemdetail.jsp per l'applicazione Storefront
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %> <tiles:insert page="../layouts/storefrontDefaultLayout.jsp" flush="true"> <tiles:put name="header" value="../common/header.jsp"/> <tiles:put name="menubar" value="../common/menubar.jsp"/> <tiles:put name="body-content" value="../catalog/itemdetail-body.jsp"/> <tiles:put name="copyright" value="../common/copyright.jsp"/> </tiles:insert>
L'unica differenza tra la pagina index.jsp nell'Esempio 14-3 e la pagina itemdetail.jsp dell'Esempio 14-4 consiste nel contenuto dell'attributo body-content. Se ancora non si convinti dei vantaggi dell'uso dei template si noti che index.jsp e Itemdetail.jsp negli Esempi 14-3 e 14-4 non specificano nulla a riguardo del contenuto che deve essere visualizzato. Entrambi fanno riferimento al file storefrontDefaultLayout.jsp, che ha il compito di mostrare a video il contenuto in un eferto formato. Se si vuole cambiare il layout del sito si deve solo cambiare il file storefrontDefaultLayout.jsp.
<taglib> <taglib-uri>/WEB-INF/tiles.tld</taglib-uri> <taglib-location>/WEB-INF/tiles.tld</taglib-location> </taglib> Dovrebbero gi esistere elementi taglib se si impiega una delle librerie dei tag standard di Struts. Ogni pagina che
deve usare la libreria dei tag di Tiles deve includere la seguente riga di intestazione:
<plug-in className="org.apache.struts.tiles.TilesPlugin" > <set-property property="definitions-config" value="/WEB-INF/tilesdefs.xml" /> <set-property property="definitions-debug" value="2" /> <set-property property="definitions-parser-details" value="2" /> <set-property property="definitions-parser-validate" value="true" /> </plug-in>
All' interno dell'elementoplug-in si possono specificare uno o pi elementiset-property per trasmettere ulteriori parametri alla classe Plugin. Il parametro di inizializzazione definitions-config specifica il file XML o.i file che contengono definizioni di Tiles. sono usati nomi multipli, devono essere separati da virgole. Se Il parametro definitions-debug specifica illivello di debug. I valori predefiniti sono: 0 1 2 Nessuna informazione di debug. Informazioni di debug minime. Informazioni di debug complete.
Il valore preimpostato 0. Il parametro definitions-parser-details indica il livello di informazioni di debug richiesto quando vengono interpretati i file di definizione. Questo valore viene passato al Commons Digester. I valori permessi sono gli stessi del parametro definitions-debug. Il valore preimpostato 0. Il parametro definitions-parser-validate specifica se il parser debba validare il file di configurazione di Tiles. I valori permessi sono true e false. Il valore preimpostato true. Esiste un parametro aggiuntivo chiamato definitions-factory-class. Si pu creare una definizione personalizzata di factory e inserire qui il nome della classe. Il nome preimpostato org.apache.struts.tiles.xmlDefinition.I18NfactorySet.
Supporto per l'internazionalizzazione Supporto di canali multipli Per un certo periodo, all'interno di Struts esistita una libreria di tag Template. Questi tag consentono un approccio molto limitato per la costruzione delle proprie pagine JSP in un'applicazione web. Anche se questi tag sono utilissimi per la separazione del contenuto di una applicazione web dal suo layout prescritto, Tiles va molto oltre e offre un insieme di funzionalit che include quello della libreria di tag Template integrandola ed espandendola con molte altre caratteristiche.
Tiles era precedentemente noto come Components ma il nome stato cambiato, dal momento che tale termine era troppo comune. La documentazione e il codice sorgente di Tiles fanno ancora riferimento al vec nome in alcune parti. chio
Cedric Dumoulin ha creato Tiles per estendere il concetto di template e offrire agli sviluppatori maggiore flessibilit e libert quando si creano applicazioni con la tecnologia JSP. Il contenuto per le web application ancora appannaggio delle pagine JSP e dei JavaBeans. Tuttavia il layout specificato all'interno di una pagina separata JSP o come si vedr in seguito, in un file XML.
Una pagina JSP di solito composta da diverse regioni, altrimenti dette tile. Non c' niente di particolare a riguardo, a parte il fatto che si tratta di una pagina progettata per essere usata con il framework Tiles che fa uso della libreria di tag Tiles. L'aspetto di maggiore importanza di Tiles la riusabilit. Questo vero sia per i layout che per l'area del contenuto. Diversamente dalla maggior parte delle pagine JSP, i componenti di Tiles sono riusati all'interno di una applicazione o anche di applicazioni diverse. Oltre a questo fatto, non c' niente di complicato in una tile. Di fatto, la maggior parte degli esempi sin qui seguiti possono essere classificati come tile, inclusi gli esempi dal 14-2 al 14-4.
Tiles mette a disposizione un superinsieme delle caratteristiche incluse dalla libreria standard di Struts Template definita da David Geary, ma porta il concetto di template ancora oltre offrendo ulteriori funzionalit.
Anche i layout sono considerati tile. Le pagine JSP e perfino le applicazioni intere possono riutilizzare i layout ed prassi comune scrivere una libreria di layout che poi sono impiegati in molti diversi progetti. Tiles offre una serie di layout standard che si possono modificare o riutilizzare a piacimento. I layout inclusi sono: Classic layout Columns layout Center layout Menu layout Tabs layout Vertical box layout Visualizza un header, un menu sinistro, un body e un footer HTML Visualizza una lista di tile in colonne multiple ciascuna delle quali visualizza i propri tile in un'ordine verticale Visualizza un header, un tile destro, un body e un footer Visualizza un menu con collegamenti Visualizza un insieme di tile in un ordine tabulato Visualizza una lista di tile in un ordine verticale
Dal momento che uno dei maggiori obiettivi di Tiles la riusabilit, si possono riutilizzare questi layout all'interno delle proprie applicazioni quasi senza modifiche. Si possono anche personalizzare i layout in qualsiasi modo si crede opportuno.
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %> <tiles:insert page="../layouts/storefrontDefaultLayout.jsp" flush="true"> <tiles:put name="header" value="../common/header.jsp"/> <tiles:put name="menubar" value="../common/menubar.jsp"/> <tiles:put name="body-content" value="../security/signin-body.jsp"/> <tiles:put name="copyright" value="../common/copyright.jsp"/> </tiles:insert>
Lo scopo dei tag put nell'Esempio 14-5 di fornire i contenuti alla tile di layout specificata nel tag insert di inclusione. I valori degli attributi name nell'Esempio 14-5 (come anche nei 14-3 e 14-4) devono essere uguali a quelli che il tile di layout si aspetta. Il tag insert pu includere opzionalmente un attributo ignore che avr l'effetto di istruire il tag a non scrivere niente se non trova un valore per l'attributo atteso. Come comportamento predefinito, viene lanciata un'eccezione quando non fornito un attributo.
Nome tag
Descrizione Aggiunge un elemento alla lista,. Crea una definizione di un componente tile. Prende il contenuto dallo scope di request che stato messo l da untag put. Visualizza il valore dell'attributo specifico per tile/componente/template allo JspWriter corrente Importa l'attributo di una tile nello specifico contesto.
initComponentDefinitions Inizializza una factory di definizione di tile insert Inserisce un componente tile. put putList useAttribute
Mette un attributo in un componente tile. Dichiara una lista che verr passata come attributo Usa un valore attributo all'interno di una pagina
Nome attributo
Descrizione II nome di un'attributo nel contesto corrente di tile/componente. Il valore di quest'attributo passato all'attributo name. Il nome del bean impiegato come valore. Il bean viene recuperato dal contesto specifico se esiste. Altrimenti viene usato il metodo pageContext.findAttribute(). Se l'attributo beanProperty specificato, il valore viene recuperato dalla corrispondente propriet del bean. Se il bean (o il valore della propriet del bean) un'istanza di una delle classi Attribute (Direct, Instance, ecc.), l'inserimento effettuato sulla base del tipo della classe. Altrimenti viene invocato sul bean il metodo toString() e la String restituita viene passata all'attributo name. II nome della propriet del bean. Se specificato il valore recuperato da questa propriet. Lo scope del contesto in cui pu essere trovato il bean. Se non specificato, viene usato il metodo pageContext.findAttribute(). Lo scope potrebbe essere un qualsiasi scope di JSP, componente o template. Negli ultimi due casi il bean viene ricercato nel contesto tile/componente/template. Una stringa che rappresenta l'URI di un tile o template. Gli attributi template, page, e component hanno esattamente gli stessi comportamenti. L'URL di un controller chiamato subito prima che la pagina sia inserita. L'URL di solito connota una action di Struts. Il controller (action) viene impiegato per preparare dati da visualizzare tramite il tile inserito. Va scelta una delle due possibilit controllerUrl o controllerClass.
attribute
beanName
beanProperty beanScope
component
controllerUrl
Il tipo della classe di un controller invocato subito prima dell'inserimento della pagina. Il controller viene usato per preparare dati che devono essere trattati dal tile inserito. Solo uno tra controllerUrl o controllerClass dovrebbe essere usato. La classe deve controllerClass implementare o estendere una delle seguenti: org.apache.struts.tiles.Controller, org.apache.struts.tiles.ControllerSupport, o org.apache.struts.action.Action.
definition
Il nome della definizione da inserire. Le definizioni sono inserite in un file centralizzato. Per ora solo le definizioni di una factory possono essere inserite all'interno di questo attributo. Per inserire
true o false. Se true, la pagina corrente viene inviata prima dell'inserimento dellatile.
Se quest'attributo true e l'attributo specificato da name non esiste, esce senza scrivere nulla. Il valore predefinito false, che provocher il lancio di un'eccezione. Il nome di un'entity da inserire. La ricerca avviene in quest'ordine: definition, attribute, e in seguito page. Una stringa che rappresenta l'URI di un tile o template. Gli attributi template, page e component si comportano allo stesso modo. Se l'utente ha il ruolo specificato, il tag viene tenuto presente; altrimenti il tag viene saltato e il contenuto non viene scritto. Una stringa che rappresenta l'URI di un tile o template. Gli attributi template, page e component si comportano allo stesso modo
Molti esempi del tag insert sono gi stati mostrati nelle pagine precedenti del capitolo.
Nome attributo
Descrizione II nome di una definizione genitore usata per inizializzare questa nuova definizione. La definizione parent viene ricercata nella factory di definizioni. II nome sotto il quale la definizione appena creata sar salvata. Attributo obbligatorio. URL del template/componente da inserire. Lo stesso di template. II ruolo da controllare prima di inserire questa definizione. Se il ruolo non definito per l'attuale utente, la definizione non viene inserita. Viene effettuato un controllo al momento dell'inserimento e non durante il processo di definizione. La variabile scope in cui verr creata la nuova definizione. Se non specificato il bean sar trattato nello scope di pagina. Stringa che rappresenta l'URL di una tile/componente/template (una pagina JSP)
Il seguente frammento illustra come usare un tag definition in una pagina JSP:
<tiles:definition id="storefront.default" page="/layouts/storefrontDefaultLayout.jsp" scope="request"> <tiles:put name="header" value="/common/header.jsp" /> <tiles:put name="menubar" value="/common/menubar.jsp" /> <tiles:put name="copyright" value="/common/copyright.jsp" /> </tiles:definition>
In seguito sar mostrato un esempio completo, nella sezione "Dichiarare definizioni in una pagina JSP" di questo capitolo.
Il contenuto tradotto letteralmente. Il contenuto incluso da un URL specificata. Name viene usato come URL.
definition
Il contenuto viene da una definizione specificata (factory). Name viene usato come nome della definizione.
Se si usa l'attributo type, dato per scontato che i tag get o insert siano all'interno della tile inserita. Se l'attributo type non specificato, il contenuto untyped, a meno che non provenga da un bean tipizzato. Impostare direct="true" equivalente a impostare type="string"
La Tabella 14-4 elenca gli attributi per il tag put. Tabella 14-4. Attributi per il tag put Nome attributo Descrizione II nome del bean impiegato per recuperare il valore. Il bean viene recuperato dal contesto specifico se esistente, altrimenti viene usato il metodo pageContext.findAttribute(). Se si specifica beanProperty, recupera il valore dalla corrispondente propriet del bean. Lo scope impiegato per ricercare il bean. Se non specificato viene usato il metodo pageContext.findAttribute(). Lo scope pu essere qualsiasi scope di JSP, "component", o "template". Nei due ultimi casi, viene ricercato il bean all'interno del contesto della tile/component/template. Il valore lo stesso dell'attributo value. Quest'attributo stato aggiunto per compatibilit con i tag template delle JSP. Come gestito il contenuto: true significa che il contenuto direttamente stampato, mentre false significa che incluso, false il valore predefinito. Questo un altro metodo di specificare il tipo di contenuto. Se direct="true", content "string"; se direct="false", content "page". Questo attributo era aggiunto per compatibilit con i tag template delle JSP. Il nome dellattributo. Questo tag viene tenuto in considerazione se l'utente riveste un certo ruolo, altrimenti viene ignorato e il contenuto non mostrato. Il role non considerato se si usa in una definizione il tag add. Tipo del contenuto: "string", "page", "template", o "definition". Se l'attributo type non viene specificato, il contenuto untyped, a meno che non provenga da un bean tipizzato. II valore dell'attributo pu essere String o Object. Il valore potrebbe essere direttamente assegnato (value="aValue") o derivare da un bean. Deve essere presente almeno un value, content o beanName.
beanName
beanProperty Il nome della propriet del bean. Se specificato, il valore viene recuperato da questa propriet. beanScope
content
name
beanName
beanProperty Il nome della propriet del bean. Se specificato, il valore viene recuperato da questa propriet.
beanScope
Lo scope impiegato per ricercare il bean. Se non specificato viene usato il metodo pageContext.findAttribute(). Lo scope pu essere qualsiasi scope di JSP, "component", o "template". Nei due ultimi casi, viene ricercato il bean all'interno del contesto della tile/component/template. Il valore lo stesso dell'attributo value. Quest'attributo stato aggiunto per compatibilit con i tag template delle JSP. Come gestito il contenuto: true significa che il contenuto direttamente stampato, mentre false significa che incluso, false il valore prestabilito. Questo tag viene tenuto in considerazione se l'utente riveste un certo ruolo, altrimenti viene ignorato e il contenuto non mostrato. Il role non considerato se si usa in una definizione il tag add Tipo del contenuto: "string", "page", "template", o "definition". Se l'attributo type non viene specificato, il contenuto untyped, a meno che non provenga da un bean tipizzato. Il valore che deve essere aggiunto. Pu essere String o Object.
true o false. Se true, la pagina corrente visualizzata prima dell'inserimento del tile.
Se questo attributo impostato a true e l'attributo specificato dal nome non esiste, non scrive nulla. Il valore predefinito false, che provoca il lancio di un'eccezione a runtime. II nome del contenuto da recuperare dallo scope del tile. Questo attributo obbligatorio. Questo tag viene tenuto in considerazione se l'utente riveste un certo ruolo, altrimenti viene ignorato.
Nome attributo
Descrizione Se impostato a true e l'attributo specificato dal nome non esiste, non scrive nulla. Il valore predefinito false, che provocher il lancio di un'eccezione a runtime. II nome dell'attributo. Questo attributo obbligatorio. Questo tag viene tenuto in considerazione se l'utente riveste un certo ruolo, altrimenti viene ignorato.
Nome attributo
Descrizione La classe della variabile dichiarata. II valore e il nome di variabile dichiarati. Se quest'attributo impostato a true e l'attributo specificato dal nome non esiste, semplicemente esce senza errori. Il valore di default false, che causer il lancio di un'eccezione a runtime. II nome dell'attributo del tile. Questo attributo obbligatorio, Lo scope dell'attributo dichiarato. Di default "page".
Nome attributo
Descrizione Se quest'attributo impostato a true e l'attributo specificato dal nome non esiste esce senza errori. Il valore predefinito false, che causer il lancio di un'eccezione a runtime. II nome dell'attributo del tile. Se non specificato, tutti gli attributi sono importati. Lo scope dell'attributo importato. Valore predefinito "page".
Nome attributo
Descrizione Se specificato, l'attributo classname della factory da creare e inizializzare. II nome del file di definizione. Questo attributo obbligatorio.
classname file
Come gi detto, ci sono due luoghi in cui si possono specificare le definizioni: una pagina JSP o un file XML. Per primo sar discusso l'approccio tramite pagine JSP. Per seguire questo approccio, si crei una pagina JSP e si dichiarino tutte le definizioni al suo interno. Per l'applicazione Storefront, si creato un file storefront-defs.jsp e si sono poste le definizioni di default al suo interno, come mostrato nell'Esempio 14-6.
Esempio 14-6. Dichiarazione di definizioni di tile in una pagina JSP
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %> <tiles:definition id="storefront.default" page="/layouts/storefrontDefaultLayout.jsp" scope="request"> <tiles:put name="header" value="/common/header.jsp" /> <tiles:put name="menubar" value="/common/menubar.jsp" /> <tiles:put name="copyright" value="/common/copyright.jsp" /> </tiles:definition>
La definizione nell'Esempio 14-6 usa lo stesso aspetto della tile prima illustrata. I file comuni prima sparsi attraverso le varie tile sono ora accentrati nel file di definizione, rendendo molto pi facile la loro modifica. Per esempio, se si volesse definire una differente pagina per il copyright, il file di definizione sarebbe l'unico luogo dove operare tale modifica. Non si dovrebbero perci modificare tutte le pagine JSP. La sintassi del tag definition somiglia a quella dei tag insert prima discussi. Si deve solo provvedere un attributo id e modificare l'attributo path come attributo page. Inoltre lo scope predefinito per il tag definition page. Si stabilito di impostare lo scope a page per offrire uno scope un po' pi vasto. Per trarre vantaggio dalle definizioni, i componenti devono potervi accedere. Dal momento che si impostato lo scope di definizione della richiesta ed esister solo per il tempo necessario a questa, occorre un modo per includere la definizione nelle diverse pagine JSP. Per fortuna gi noto il modo di includere una pagina JSP in un'altra pagina usando la direttiva JSP include. L'Esempio 14-7 illustra la pagina signin.jsp con l'impiego del file di definizione delle JSP.
Esempio 14-7. La pagina signin.jsp che impiega una definizione di tile
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %> <%@include file="../common/storefront-defs.jsp" %> <tiles:insert beanName="storefront.default" beanScope="request"> <tiles:put name="body-content" value="../security/signin-body.jsp"/> </tiles:insert>
Con quest'approccio i componenti di tile devono solo inserire il contenuto specifico per la pagina. Si paragoni l'Esempio 14-7 all'Esempio 14-5. Si noter allora che il file signin.jsp che usa le definizioni deve solo mettere a disposizione contenuti specifici per la pagina, il file sign-body.jsp.
"-//Apache Software Foundation//DTD Tiles Configuration//EN" "http://jakarta.apache.org/struts/dtds/tiles-config.dtd"> <tiles-definitions> <definition name="storefront.default" path="/layouts/storefrontDefaultLayout.jsp"> <put name="header" value="/common/header.jsp" /> <put name="menubar" value="/common/menubar.jsp" /> <put name="copyright" value="/common/copyright.jsp" /> </definition> </tiles-definitions>
Non esiste molta differenza tra il formato delle definizioni nel file JSP dell'Esempio 14-6 e nel file XML dell'Esempio 14-8. Il file XML impiega una sintassi appena differente. I due formati sono comunque abbastanza simili da causare problemi.noti che nelle definizioni Si JSP si usa il tag put:
<tiles:put name="body-content" value="../security/signin-body.jsp"/> ma nelle definizioni XML si usa l'elemento : put <put name="header" value="/common/header.jsp" />
Ci si assicuri di non confondere i due dal momento che potrebbe essere difficile da scoprire un bug a posteriori. Ogni definizione dovrebbe avere un nome unico. I tag e le pagine JSP adoperano il nome per recuperare la definizione. Tuttavia l'uso differisce da quello degli URL dal momento che solo un nome logico per la definizione.
<tiles-definitions> <definition name="storefront.default" path="/layouts/storefrontDefaultLayout.jsp"> <put name="header" value="/common/header.jsp" /> <put name="menubar" value="/common/menubar.jsp" /> <put name="copyright" value="/common/copyright.jsp" /> </definition> </tiles-definitions> <tiles-definitions> <definition name="storefront.custom" extends="storefront.default"> <put name="copyright" value="/common/new-copyright.jsp" /> </definition> </tiles-definitions>
Nell'Esempio 14-9, sono ereditati tutti gli attributi nella definizione storefront.default. Tuttavia la definizione storefront.customer ignora il valore dell'attributocopyright con una pagina alternativa di copyright. Questa una caratteristica molto potente. Se si hanno definizioni figlie multiple che estendono tutte una definizione centrale, il cambiamento della definizione centrale si riflette su tutte le definizioni figlie. Vale a dire, cambiando il layout nella definizione centrale, il cambiamento si riflette in tutte le altre.
Le definizioni di Tiles possono essere usate anche come forward di Struts, invece dei normali URL. Per poter usare le definizioni in tal modo, si creino prima le definizioni:
<tiles-definitions> <definition name="storefront.default" path="/layouts/storefrontDefaultLayout.jsp"> <put name="header" value="/common/header.jsp" /> <put name="menubar" value="/common/menubar.jsp" /> <put name="copyright" value="/common/copyright.jsp" /> </definition> <definition name="storefront.superuser.main" extends="storefront.default"> <put name="header" value="/common/super_header.jsp" /> <put name="menubar" value="/common/super_menubar.jsp" /> <put name="copyright" value="/common/copyright.jsp" /> </definition> </tiles-definitions>
Questo frammento illustra due definizioni, la definizione standard di default e una seconda che definisce il layout per un eventuale "superutente". Un superutente potrebbe essere un cliente abituale che compie molte ordinazioni per cui deve avere la possibilit di poter usare pi opzioni per una maggior facilit d'uso. Nel file di configurazione di Struts vanno definiti poi i forward che usano tali definizioni:
Una volta che si creata la definizione specifica per la localizzazione e la si posta nella directory WEB-INF, l'unica altra cosa da fare assicurarsi che una Locale venga immagazzinata nella HttpSession dell'utente. Tiles dipende dalla stessa istanza di Locale che Struts usa per determinare quale sia il file di definizioni da impiegare. Ci si deve assicurare che Struts conservi le impostazioni locali dell'utente nella sessione. Questo obiettivo raggiunto impostando l'attributo locale a true nell'elemento controller. Si veda "11 DTD di configurazione di Struts" nel Capitolo 4 per maggiori ragguagli sull'elemento controller. Questo quanto c'era da dire su Tiles. Si noti che si deve continuare a fare affidamento sul resource bundle di Struts per quanto riguarda le risorse locale-sensitive, quali testo, messaggi e immagini. Il supporto in Tiles di I18N dvrebbe essere sempre usato per differenze nel layout basate sulla localizzazione. Struts e Tiles funzionano molto bene insieme al fine di produrre un completo supporto di I18N.
CAPITOLO 15
In quanto programmatori Java diligenti e appassionati, vorremmo illuderci che il nostro software potr superare anche l'esame pi impegnativo. Tuttavia, dato che tutti possono sbagliare, non mai buona abitudine credere che il proprio software sia immune da difetti. Diventa quindi importante usare qualche strumento per eliminare i difetti o almeno per ridurli al minimo accettabile. La generazione di messaggi di log pu aiutare a individuare i difetti presenti nei propri programmi, ma anche importante per altri motivi. Ad esempio, la sicurezza e il controllo possono dipendere dal logging per raccogliere informazioni per gli amministratori di sistema riguardo a cosa stanno facendo gli utenti autorizzati e soprattutto quelli non autorizzati. Con informazioni in tempo reale circa potenziali attacchi alla sicurezza dell'applicazione, il logging pu dare un margine molto utile agli amministratori di sistema e permettere reazioni pi rapide. Questo capitolo prende in esame da vicino l'impiego del logging nelle applicazioni Struts e come possa essere utile per identificare difetti prima che l'applicazione entri in produzione oppure, se il software gi in produzione, come possa identificare velocemente i problemi per giungere a una soluzione
public void log( String msg ); public void log( String msg, Throwable throwable ); Si possono usare questi metodi tramite ServletContext passando i corretti argomenti. L'Esempio 15-1 illustra come farlo tramite una Action di Struts
Esempio 15-1. LoginAction: uso di ServletContext per il logging
public class LoginAction extends StorefrontBaseAction { public ActionForward execute( ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response ) throws Exception{ // Get the user's login name and password. They should have already been // validated by the ActionForm. String email = ((LoginForm)form).getEmail( ); String password = ((LoginForm)form).getPassword( ); // Obtain the ServletContext ServletContext context = getServlet().getServletContext( // Log which user is trying to enter the site context.log( "Login email: " + email ); // Log in through the security service IStorefrontService serviceImpl = getStorefrontService( ); UserView userView = serviceImpl.authenticate(email, password); // Log the UserView for auditing purposes context.log( userView.toString( ) ); UserContainer existingContainer = null; HttpSession session = request.getSession(false); if ( session != null ){ existingContainer = getUserContainer(request); session.invalidate( ); }else{ existingContainer = new UserContainer( ); } // Create a new session for the user session = request.getSession(true); existingContainer.setUserView(userView); session.setAttribute(IConstants.USER_CONTAINER_KEY, existingContainer); return mapping.findForward(IConstants.SUCCESS_KEY); } } LoginAction dell'Esempio 15-1 mostra una semplice applicazione per l'invio di messaggi di log al file di log del container. Invoca il metodo log() e passa un messaggio come stringa di lettere che viene scritto nel log. Come
precedentemente affermato, nome e posizione del log dipendono dal web container in uso. Alcuni di questi possono assegnare un file di log separato per ciascuna applicazione mentre altri possono usare un singolo file di log. Se viene usato un file di log singolo i messaggi di web application diverse finiranno nello stesso file, in genere preceduti dal nome dell'applicazione.
);
Tramite i filtri, gli sviluppatori di servlet possono compiere le seguenti azioni: Accedere a una risorsa web prima che sia invocata una request a questa. Elaborare una request per una risorsa prima che sia invocata. Modificare header e dati inglobandola request in una versione personalizzata dell'oggetto della request. Modificare gli header e i dati di response inglobando la response in una versione personalizzata dell'oggetto della response. Intercettare una chiamata a un metodo su una risorsa prima che sia compiuta. Compiere azioni su una servlet o sii gruppi di servlet tramite uno o pi filtri attivi in un certo ordine. Sulla base dei compiti richiesti da sviluppatori di servlet, le specifiche delle servlet descrivono parecchi tipi possibili di filtri: Filtri per autenticazione Filtri per controllo e logging Filtri per conversione delle immagini Filtri per compressione dei dati Filtri per cifratura Filtri per tokenizing Filtri che innescano eventi basati su accesso a una risorsa Filtri XSLT per trasformare contenuti XML Filtri MIME a catena Filtri per mantenere in cache URL e altre informazioni Tutti questi tipi di filtri sono interessanti, ma in questo capitolo ci si occuper di quelli per il logging e il controllo. Con i filtri possibile registrare messaggi con qualsiasi dato contenuto negli oggetti di response e request. Dal momento che il filtro associato strettamente al container delle servlet, sar probabilmente necessaria la funzionalit di logging da altre parti nella propria applicazione. L'uso di filtri di solito non sufficiente per la totalit dell'applicazione. Tuttavia i filtri sono perfetti per controllare o seguire le azioni di un utente nel sistema. Se si vuole avere un'idea di quali parti del proprio sito siano le pi frequentate o di dove vanno alcuni gruppi di utenti, i filtri costituiscono la scelta ideale. Possono anche fornire informazioni a riguardo di dati specifici cui gli utenti accedono pi di frequente. Per esempio, poniamo di avere un'applicazione tipo catalogo in linea e di voler sapere quali articoli gli utenti visionano pi spesso. Dal momento che gli oggetti di richiesta e risposta contengono questa informazione, si pu facilmente seguire questa informazione e immetterla in un database per successive analisi.
public void init(FilterConfig filterConfig) throws ServletException; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; public void destroy( );
Il web container invoca il metodo init() quando pronto a porre in opera il filtro. In questo metodo si pu inizializzare qualsivoglia risorsa. Il metodo destroy() l'opposto di init(). Il web container invoca questo metodo quando chiama fuori servizio il filtro. A questo punto si dovrebbero ripulire le risorse aperte che il filtro potrebbe avere utilizzato, ad esempio le connessioni ai database. Infine, il web container chiama il metodo doFilter() ogni volta che una richiesta viene recepita e il container determina che dovrebbe essere avvisata l'istanza filtro. Qui il luogo dove porre qualsivoglia funzionalit il filtro debba svolgere. L'Esempio 15-2 illustra un filtro che potrebbe essere usato per effettuare il logging di eventi nel file di log delle servlet o per inizializzare un servizio di log terze parti.
Esempio 15-2. Una classe servlet filtro
import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletRequest; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; import javax.servlet.ServletException; /** * An Esempio servlet filter class. */ public class LoggingFilter implements Filter{ public final static String LOG_FILE_PARAM = "log_file_name"; private FilterConfig filterConfig = null; private ServletContext servletContext = null; public void init( FilterConfig config ) throws ServletException { // Initialize any neccessary resources here this.filterConfig = config; this.servletContext = config.getServletContext( ); // You can get access to initialization parameters from web.xml // although this Esempio doesn't really use it String logFileName = config.getInitParameter( LOG_FILE_PARAM ); // You can log messages to the servlet log like this log( "Logging to file " + logFileName ); // Maybe initialize a third-party logging framework like log4j } public void doFilter( ServletRequest request, ServletResponse response, FilterChain filterChain )
throws IOException, ServletException { // Log a message here using the request data log( "doFilter called on LoggingFilter" ); // All request and response headers are available to the filter log( "Request received from " + request.getRemoteHost( ) ); // Call the next filter in the chain filterChain.doFilter( request, response ); } public void destroy( ){ // Remove any resources to the logging framework here log( "LoggingFilter destroyed" ); } protected void log( String message ) { getServletContext( ).log("LoggingFilter: " + message ); } protected ServletContext getServletContext( return this.servletContext; } }
Proprio come si deve stare attenti all'uso di thread multipli nelle servlet Java, si deve porre attenzione a non violare con i filtri qualsiasi pratica di salvaguardia dei thread. Il servlet container potrebbe inviare thread concorrenti a una sola istanza di una classe filtro e ci si deve assicurare di non creare niente che causi problemi tra i thread. In altri termini, nessun dato specifico per il client dovrebbe essere conservato in variabili di istanza. Le variabili locali vanno bene, proprio come le servici Java, dal momento che sono conservate all'interno dello stack invece che nellheap.
){
Ogni volta che il web container riceve una richiesta per la risorsa MyEsempioServlet, verr invocato il metodo doFilter() nella classe LoggingFilter. La seguente porzione XML mostra come il filtro possa essere mappato su tutte le request inviate alla web application:
Descrizione
Interfaccia listener
richiesta oppure sta per essere spento dal container ServletContextListener delle servlet Sono stati aggiunti, rimossi o modificati attributi di
ServletContext
ServletContextAttributesListener
Descrizione
Interfaccia listener
Un oggetto HttpSession stato creato, invalidato o il HttpSessionListener tempo scaduto. Sono stati aggiunti, rimossi o modificati attributi di
HttpSession.
HttpSessionAttributesListener
import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; /** * An Esempio event listener class that * initializes a logging service. */ public class LoggingListener implements ServletContextListener{ private ServletContext context = null; /** * Called by the container before the first request is * processed. This is a good time to initialize * the logging service. */ public void contextInitialized( ServletContextEvent event ){ this.context = event.getServletContext( ); // Initialize the logging service here // Log a message that the listener has started log( "LoggingListener initialized" ); } /** * Called by the container when the ServletContext is about * ready to be removed. This is a good time to clean up * any open resources. */ public void contextDestroyed( ServletContextEvent event ){ // Clean up the logging service here // Log a message that the LoggingListener has been stopped log( "LoggingListener destroyed" ); } /** * Log a message to the servlet context application log or * system out if the ServletContext is unavailable. */ protected void log( String message ) { if (context != null){ context.log("LoggingListener: " + message ); }else{ System.out.println("LoggingListener: " + message); } } }
La classe event-listener dell'Esempio 15-3 ha due metodi invocati dal web container:
import javax.servlet.ServletContextListener; import javax.servlet.ServletContextEvent; /** * An Esempio event listener class that * initializes the database connection pooling. */ public class DBConnectionPoolListener implements ServletContextListener{ /** * Called by the container before the first request is * processed. This is a good time to initialize * the connection pooling service. */ public void contextInitialized( ServletContextEvent event ){ // Initialize the connection pooling here } /** * Called by the container when the ServletContext is about * ready to be removed. This is a good time to clean up * any open resources. */ public void contextDestroyed( ServletContextEvent event ){ // Shut down the connection pooling and open database connections } }
Dal momento che sia LoggingListener che DBConnectionPoolListener sono in ascolto dello stesso tipo di eventi dell'applicazione il servlet container le informer nell'ordine in cui sono elencate nel descrittore di deploy sulla base del tipo dell'evento.
<web-app> <listener> <listener-class>LoggingListener</listener-class> </listener> <listener> <listener-class>DBConnectionPoolListener</listener-class> </listener> <servlet> <servlet-name>EsempioServlet</servlet-name> <servlet-class>EsempioServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>EsempioServlet</servlet-name>
<url-pattern>/Esempio</url-pattern> </servlet-mapping> </web-app> LoggingListener sar avvertito per prima, subito dopo verr informata l'istanza di DBConnectionPoolListener. Quando verr terminata la web application, i listener saranno informati in ordine opposto. I listener degli eventi di HttpSession vengono informati prima dei listener relativi al contesto
dell'applicazione.
L'estensibilit solo un'altra delle potenti caratteristiche di questa libreria. Se si impiega un package di logging non ancora supportato, si pu creare un adattatore per tale implementazione estendendo i componenti appropriati, e la nostra applicazione potr usarla.
dell'applicazione. Ci sono molti modi per farlo, ma il pi semplice consiste nella creazione di un file di propriet chiamato commons-logging.properties che comprende i nomi delle classi di implementazione di logging presenti. L'elemento chiave di maggiore importanza in questo file la chiave org.apache.commons.logging.Log. impiegata per definire la classe di implementazione. Quanto segue illustra come impostare il package Commons Logging affinch utilizzi l'implementazione SimpleLog:
org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
Durante il funzionamento i componenti di logging cercheranno il file commons-logging. properties e tenteranno di istanziare il nome completo di classe qui trovato. Il nome della classe specificata deve essere disponibile al class loader della web application. Il file di propriet deve essere inserito nella directory WEB-INF/classes. Per il passaggio a log4j, si deve soltanto cambiare il nome della classe nel filecommons-logging.properties:
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JCategory Log
Si noti che si deve ancora configurare log4j per il proprio ambiente, inclusa la creazione di un file log4j.properties. Ogni implementazione di logging potrebbe avere differenti requisiti di configurazione che devono essere soddisfatti.
public static Log getLog(Class class); public static Log getLog(String name) Entrambi i metodi getLog() restituiscono un oggetto che implementa l'interfaccia org.apache.commons.logging.Log. Per creare un'istanza di Log da usare all'interno della classe LoginAction, per esempio, si potrebbe passare il nome della classe al metodo getLog(): Log log = LogFactory.getLog( LoginAction.class );
L'istanza di Log sarebbe allora disponibile per la classe LoginAction:
if (log.isInfoEnabled( )){ // Log which user is trying to enter the site log.info( "Login email: " + email ); }
L'interfaccia Log implementa i metodi di logging che si possono usare per inviare messaggi di log a una certa destinazione. I seguenti sono i pi importanti:
StringBuffer buf = new StringBuffer( ); buf.append( "Login Successful - " ); buf.append( "Name: " ); buf.append( userView.getFirstName( ) ); buf.append( " " ); buf.append( userView.getLastName( ) ); buf.append( " - " ); buf.append( "Email: " );
buf.append( userView.getEmailAddress(
) );
if ( log.isDebugEnabled( ) ){ StringBuffer buf = new StringBuffer( ); buf.append( "Login Successful - " ); buf.append( "Name: " ); buf.append( userView.getFirstName( ) ); buf.append( " " ); buf.append( userView.getLastName( ) ); buf.append( " - " ); buf.append( "Email: " ); buf.append( userView.getEmailAddress( ) ); // Log the UserView for auditing purposes log.debug( buf.toString( ) ); }
In questo caso, l'applicazione non perde tempo a creare StringBuffer che poi non user.
Secondo i suoi creatori, log4j stato creato con due concetti in mente: velocit e flessibilit. Una delle caratteristiche principali dell'infrastruttura di logging costituita dalla propria nozione di ereditariet per le categorie o logger, come ora vengono chiamate. log4j supporta una relazione genitore/figlio tra i logger configurati nell'ambiente. Per esempio, se si configurato un logger nel package com.oreilly.struts e un'altro logger per tutte le classi in com.oreilly.struts.storefront, il primo logger sar il padre del secondo. Questa struttura gerarchica permette la flessibilit di controllo sui messaggi di log scritti sulla base di elementi come la struttura del package.
Non si deve andare molto pi in l se i requisiti non lo richiedono. Se lo si desidera, si potr configurare un singolo logger per l'intero ambiente. Si pu configurare log4j per i propri specifici fini e cambiarlo quando si desidera, semplicemente modificando un file di configurazione esterno, senza la necessit di cambiare il codice dell'applicazione. Ci sarebbe da riempire un libro discutendo di log4j. Si dar per scontato che si abbia familiarit con i concetti base e si affronteranno solo gli aspetti essenziali a riguardo dell'integrazione di log4j con Struts. Se non si ha ancora familiarit con log4j, un buon momento per acquisirla. Per dettagli pi approfonditi si pu scaricare o leggere la documentazione presso lURL http://jakarta.apache.org/log4j.
# A basic log4j configuration file that creates a single console appender # Create a single console appender that logs INFO and higher log4j.rootLogger=INFO, stdout # ConFigura the stdout appender to go to the console log4j.appender.stdout=org.apache.log4j.ConsoleAppender # ConFigura the stdout appender to use the PatternLayout log4j.appender.stdout.layout=org.apache.log4j.PatternLayout # Pattern to output the caller's filename and line number. log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
Il semplicissimo file di configurazione dell'Esempio 15-5 illustra l'impostazione di un solo appender, in questo caso ConsoleAppender, che dirige i messaggi di log aSystem.out. Questo file log4j.properties deve essere installato nella directory WEB-INF/classes in modo tale che log4j possa trovarlo e utilizzarlo per configurare l'ambiente di logging della web application. Se si hanno diversq web application, e possibile configurare un file log4j.properties per ciascuna di esse. II file di configurazione di log4j mostrato dell'Esempio 15-5 invia messaggi di log a una destinazione singola, la console. Tuttavia, si possono configurare diverse destinazioni per i file di log e perfino inviare messaggi ai certe destinazioni sulla base del livello di log prescelto. L'Esempio 15-6 illustra un semplice esempio con due appender.
Esempio 15-6. Un file di configurazione di log4j che usa due appender
# A sample log4j configuration file # Create two appenders, one called stdout and the other called rolling log4j.rootLogger=DEBUG, stdout, rolling # ConFigura the stdout appender to go to the console log4j.appender.stdout=org.apache.log4j.ConsoleAppender # ConFigura the stdout appender to use the PatternLayout log4j.appender.stdout.layout=org.apache.log4j.PatternLayout # Pattern to output the caller's filename and line number log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n # ConFigura the rolling appender to be a RollingFileAppender log4j.appender.rolling=org.apache.log4j.RollingFileAppender # ConFigura the name of the logout for the rolling appender log4j.appender.rolling.File=output.log # Set up the maximum size of the rolling log file log4j.appender.rolling.MaxFileSize=100KB # Keep one backup file of the rolling appender log4j.appender.rolling.MaxBackupIndex=1 # ConFigura the layout pattern and conversion pattern for the rolling appender log4j.appender.rolling.layout=org.apache.log4j.PatternLayout log4j.appender.rolling.layout.ConversionPattern=%d{ABSOLUTE} - %p %c - %m%n
II file di configurazione di log4j dell'Esempio 15-6 crea un appender che invia messaggi alla console, come gi nell'Esempio 15-5e un altro appender che li invia al file output.log. Di nuovo ripetiamo che non si vogliono trattare tutte le possibilit di logging di log4j maggiori informazioni possono essere reperite sul sito di log4j.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="stdout" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%5p [%t] (%F:%L) - %m%n"/> </layout> </appender>
<root> <priority value ="INFO" /> <appender-ref ref="stdout" /> </root> </log4j:configuration>
Al solito, si porr il file XML nella directory WEB-INF/classes come gi per il file log4j. properties. Tuttavia si dovr impostare la propriet di sistema log4j.configuration in conformit con il nome del file, in maniera che log4j possa sapere quale file caricare. Non ci sono nomi di file predefiniti per il formato XML. Ci non stato necessario con il file di propriet dal momento che il nome log4j.properties parte dell'inizializzazione prestabilita di log4j. Se tale file viene localizzato in qualsiasi punto del classpath, esso verr usato per la configurazione dell'ambiente di log4j. Ci sono molti modi per impostare le propriet di configurazione di log4j.confguration e i vari container possono provvedere metodi alternativi. In Tomcat 4.0, ad esempio, si pu
In Tomcat Version 4.0, for Esempio, you can set a variable called CATALINA_OPTS in the catalina.bat file to provide this information to the logging environment. For Esempio:
set CATALINA_OPTS=-Dlog4j.configuration=log4j.xml
When you start up Tomcat, the log4j environment will then be able to locate the XML configuration file. Other containers may provide alternate methods for setting the value, but you can always set the value on the Java command line as a system property. You will probably need to modify the container's startup script using this approach, however:
java -Dlog4j.configuration=log4j.xml
If the log4j environment is unable to find a valid configuration file, either properties-based or XML-based, you will see something similar to the following message when you first attempt to initialize the logging environment:
log4j:WARN No appenders could be found for logger XXX. log4j:WARN Please initialize the log4j system properly. The XXX in the first message will actually show the name of the logger for which no appenders were conFigurad.
java -Dlog4j.configuration=log4j.xml
Most containers use a separate class loader for each web application, and some containers may not allow the web applications to know about classes or JARs loaded by the container itself. However, if you need to use an absolute path, you can specify one like this:
java -Dlog4j.configuration=file:/c:/dev/env/log4j.xml
Be careful when using an absolute pathbecause the configuration file is not relative to a web application, all web applications will share the same one. Generally speaking, specifying a relative path is much more flexible than using an absolute path because you can't always guarantee the directory structure of all your target environments.
It's a good idea to periodically back up the log files and start again with an empty log file. For some production environments, this "rollover" period may be nightly; others may only need to perform this routine weekly. Unless you can shut down the application while you back up the log files, it's very cumbersome to back them up manually. Fortunately, log4j provides a type of appender that automatically swaps the log file out with a new one while at the same time maintaining a backup of the old log file. The org.apache.log4j.DailyRollingFileAppender class provides the ability to log to a file and roll over or back up the log file while the application is still running. You can also specify the rollover frequency and the date pattern that will be used for the backup names. Having this functionality available out of the box makes log4j invaluable to any application that needs to roll over log files at user-defined intervals.
<%@ page import="org.apache.commons.logging.Log" %> <%@ page import="org.apache.commons.logging.LogFactory" %> <%-- Get a reference to the logger for this class --%> <% Log logger = LogFactory.getLog( this.getClass( ) ); %> <% logger.debug( "This is a debug message from a jsp" ); %> <html> <head> <title>Using Commons Logging in a JSP page</title> </head> <body> <% logger.info( "This is another log message in the jsp" ); %> There should be two log messages in the log file. </body> </html>
L'ambiente Commons Logging deve essere configurato bene perch questo funzioni proprio come quando lo si impiega per le classi Java. Qualsiasi pagina JSP che sia parte della web application potr usare le utilit di logging. Dal momento che molti container usano un differente class loader per ciascuna web application, qualsiasi pagina JSP che non faccia parte della web application configurata per log4j potrebbe non essere in grado di usare le utility di logging. Anche se l'Esempio 15-8 mostra come possa essere facile l'impiego dell'APl Commons Logging nelle pagine JSP, ci sono alcuni problemi legati a questo approccio. Il pi evidente che le proprie pagine JSP conferanno codice Java. Come gi detto pi volte. alcuni sviluppatori la considerano una prassi da evitare. Per fortuna c' un altro modo. La libreria di tag Jakarta Taglibs contiene una libreria di tag personalizzati JSP progettata per log4j e chiamata Log. Per informazioni e per scaricare questa libreria si veda l'URL http://jakarta.apache.org/taglibs/doc/log-doc/intro.html. Proprio come una comune libreria di tale, anche questa va configurata per la propria web application, mettendo il file log.tld nella directory WEB-INF e il file JAR Log nella directory WEB-INF/lib. Si dovr anche aggiungere il corretto elemento taglib al descrittore di deploy della propria web application, come per le librerie di tag di Struts:
Una volta che il tag personalizzato Log installato e configurato, pu essere impiegato nelle proprie pagine JSP come da Esempio 15-9.
Esempio 15-9. Una pagina JSP di esempio con l'uso del tag Log
<%@ taglib uri="/WEB-INF/log.tld" prefix="logger" %> <logger:debug message="This is a debug message from a jsp using the Log tag" /> <html> <head> <title>Using the Log Tag in a JSP page</title> </head> <body> <logger:info message="This is another message using the log4j tag" /> There should be two log messages in the log4j log file. </body> </html>
Si noti che non c' traccia di codice Java nell'Esempio 15-9. Tale approccio, anche se non troppo differente dall'Esempio 15-8, risulta molto pi pulito con pagine JSP grandi pi e complesse. Ancora, il tag Log offre la funzionalit di dumping degli oggetti conservati nello scope della pagina, della sessione o dell'applicazione. Ci utilissimo quando si svolge il debug della propria web application. L' Esempio 15-10 illustra come sia facile.
Esempio 15-10. Uso della libreria di tag Log per il dumping di informazioni
<%@ taglib uri="/WEB-INF/log.tld" prefix="logger" %> <html> <head> <title>Using the Log Tag in a JSP page</title> </head> <body> <logger:dump <logger:dump <logger:dump <logger:dump scope="page"/> scope="request"/> scope="session"/> scope="application"/>
The page, request, session, and application dumps should be in the log file. </body> </html>
Sfortunatamente il tag Log non funziona ancora con il package Commons Logging. Dipende dall'implementazione di log4j. Tuttavia sempre un tag prezioso se si vuole ottenere un'informazione pi accurata per il debug delle JSP.
logger.debug("Session user.getFullName());
id
is:
"
sessId
"
for
user
"
si perde molto tempo per il logging. log4j non ha a che fare con questo; piuttosto si tratta del costo associato all'invocazione, di metodi Java e alla concatenazione di stringhe prima della generazione della reale definizione di log. Infine, come gi riportato, i logger possono essere connessi in relazioni tipo padre/figlio. Dal momento che il livello di logging pu essere assegnato a qualsiasi livello, l'ambiente di log4j dovrebbe risalire la gerarchia per determinare se il messaggio di log pu essere scritto. Se questa gerarchia molto profonda, il tempo speso va a detrimento della creazione del messaggio di log. Solitamente, in fase di sviluppo della propria applicazione i costi di tempo del logging non sono tanto importanti. La prestazione del meccanismo di logging non dovrebbe avere troppa importanza durante lo sviluppo e il debug dell'applicazione; di fatto si desidera in questi stadi quanto pi logging possibile. In test o produzione per necessario abbassare notevolmente il livello di logging. Con altri linguaggi, il codice che si occupa del logging potrebbe non essere neanche presente nei file binari, ma venire rimosso per rendere i file binari pi "leggeri" e per non fare visualizzare i messaggi di log. Questo non necessario con log4j, dal momento che, con opportuni cambiamenti al file di configurazione, si pu controllare quanto logging effettuare (ma nei file binari ci saranno comunque le indicazioni per il logging)
Ci sono anche diversi altri tipi di appender creati da altri sviluppatori che potrebbero tornare utili. Si possono trovare queste estensioni di terze parti in http://jakarta.apache.org/log4j/docs/download.html. Vale davvero la pena di dare un'occhiata a quello che si trova in questo URL. La lista di estensioni disponibili in continua crescita.
CAPITOLO 16
Contrariamente a quello che pensano molti sviluppatori, progettare e compilare un'applicazione sono solo la met dell'opera. Terminata la sua realizzazione, l'applicazione deve ancora essere confezionata in package e ne deve essere effettuato il deploy nell'ambiente di base. Molte applicazioni sono scritte per essere inserite nell'ambiente di produzione di un cliente. Per altre, l'ambiente di destinazione quello interno. Fortunatamente, per le web application il lavoro da compiere in entrambi i casi molto simile. I deploy interni possono essere meno formali e meno stressanti, ma in ogni caso vanno affrontati con seriet, in maniera efficiente, e professionale. Un cattivo deploy lascia una cattiva impressione sia al cliente vero e proprio sia a un altro dipartimento della nostra organizzazione. Formalizzare il processo ali packaging e deploy permette agli sviluppatori di concentrarsi sulla scrittura di un'applicazione di qualit e di risparmiare tempo per capire se l'applicazione si installer e funzioner correttamente una volta realizzata. Questo capitolo tratta i metodi migliori per il packaging e il deploy di un'applicazione basata su Struts, includendo l'automazione del processo di compilazione. Sar dato speciale rilievo ad Ant, il tool di compilazione basato su Java disponibile in Jakarta.
16.1.1 Packaging
L'operazione di packaging di una applicazione basata su Struts include la raccolta di tutti i file e delle risorse che fanno parte dell'applicazione e la loro collocazione in una struttura logica. In un'applicazione Struts sono di solito inclusi molti tipi differenti di risorse necessarie all'applicazione, quali ad esempio:
files HTML e/o XML Immagini e file audio/video Stylesheets JavaServer Pages File di propriet Classi di utilit Java File di configurazione Classi Action e ActionForm JAR di terze parti Durante la fase di progettazione si deve pensare bene a come strutturare l'applicazione. Non si deve certo eccedere nell'immaginare ogni dettaglio, ma si devono comprendere bene i requisiti dell'ambiente di destinazione e quanto questo possa influenzare la strategia di packaging e deploy. Si dovrebbero prendere decisioni sul package principale e sulla struttura della directory dell'applicazione prima della scrittura: ci pu ovviare ai normali fraintendimenti tra sviluppatori e ridurre il numero di file di risorse ridondanti.
16.1.2 Deploy
Come gi detto, il packaging e il deploy sono strettamente associati ma prevedono compiti diversi: il packaging determina dove risiederanno e come verranno ordinati i file delle risorse all'interno della struttura del package; il deploy si occupa del modo in cui le web application vengono installate e configurate all'interno di un web container. Esistono due modalit per effettuare il deploy di una web application all'interno di un container. Il primo modo prevede il deploy in un file di tipo web archive (WAR). La maggior parte dei web container installano un file WAR e lo mettono a disposizione degli utenti, spesso senza richiedere un riavvio del container. Questo un modo preferibile, dal momento che una volta che il file WAR composto nel modo corretto il resto del lavoro effettuato dal container. Uno degli svantaggi sta nel fatto che, per qualsiasi cambiamento, il file .war deve essere ricreato e ne deve essere rieseguito il deploy. In seguito si discuter di come effettuare il deploy delle applicazioni Struts come file WAR. La seconda modalit richiede pi lavoro da parte dello sviluppatore. Rimane la necessit di generare un file WAR con la propria web application ma occorre anche lo "spacchettamento" manuale del file WAR in una directory del container. L'esatta collocazione di una directory dipende dal container. In Tomcat e Resin, per esempio, la directory predefinita per le web application webapps. In WebLogic si devono decomprimere i file WAR sotto la directory applications. Tutti questi container permettono di specificare directory di installazione alternative. Quando si decomprime un file WAR, si deve creare una directory nella quale effettuare "l'apertura del pacchetto" contenente i file. Il nome della directory di solito quello della web application. Per fare un esempio, se la web application fosse Storefront e volessimo installarla sotto Tomcat, si potrebbe creare una directory chiamata storefront nella directory webapps e l estrarre i file dall'archivio WAR. Il file WAR in questione non dovrebbe contenere la directory storefront come parte della propria struttura. Questo approccio di deploy si definire come exploded directory format (formato di directory esplosa), dal momento che il file WAR aperto nella sua struttura originale all'interno del container. I vantaggi di quest'approccio stanno nel fatto che, se vi sono dei cambiamenti, devono essere sostituiti solo i file necessari e non occorre un nuovo deploy: modalit comoda in fase di sviluppo o debug ma sconsigliata per la produzione. Quando si effettua il deploy di una web application in produzione consigliabile lasciare il file WAR "impacchettato", perch cos si evita pi facilmente di creare confusione nei file. In questo modo, infatti, deve essere sostituito l'intero file WAR, e quindi non c' possibilit di perdita di sincronizzazione tra la versione di un singolo file e il resto dell'applicazione.
di contribuire all'identificazione di entit come le classi Java. Quando estendiamo Struts con le nostre classi e interfacce, occorre decidere come effettuare al meglio il packaging di questi componenti per la nostra applicazione. Non tutte le applicazioni conterranno le stesse classi e interfacce.
[1]
Qualsiasi classe Java che non abbia un package dichiarato nel file sorgente viene considerata come appartenente al package predefinito.
<security-constraint> <web-resource-collection> <web-resource-name>SecureOrderJSP</web-resource-name> <description>Protect the Order JSP Pages </description> <url-pattern>/order/*</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name></role-name> </auth-constraint> </security-constraint>
La Figura 16-1 mostra cosa succede se un client cerca di accedere direttamente alla pagina JSP nella directory order.
Figura 16-1. L'errore 500 che si verifica tentando l'accesso a una JSP protetta
Quando si aggiunge l'elemento security-constraint al file web.xml per l'applicazione Storefront, nessun utente non autenticato pu accedere alla risorsa specificata. In questo esempio, l'elemento role-name stato intenzionalmente lasciato vuoto per cui non incontrer mai un vero role. Potremmo anche aver specificato che solo gli utenti con un role di tipo "admin" possono accedere alle pagine, senza poi mai concedere tale ruolo a un utente reale. Il container potrebbe ancora accedere a queste pagine tramite forward e include. Occorre fare attenzione a ci che si mette nell'elemento url-pattern. Se si hanno risorse come immagini in sottodirectory, esse non saranno disponibili quando il browser prova a scaricarle. L'uso del pattern /order/* significa che non pu essere richiesto direttamente dal client niente che stia sotto la directory order, incluse le immagini nelle sottodirectory, e il browser avr difficolt a ricevere le immagini da un messaggio di response HTTP.
Il container JSP usa un processo in due tempi. Per prima cosa la pagina JSP tradotta dal codice sorgente JSP in un file sorgente Java. Ogni container potrebbe avere una propria implementazione per svolgere questo compito. Il secondo passo prende il file sorgente Java e lo compila in un file classe di servlet usando qualsiasi compilatore Java trovi installato per il JSP container. Anche se la "compilazione sul momento" una buona caratteristica mentre si opera il debug o lo sviluppo di un'applicazione, potrebbe esserci l'intenzione di non rendere disponibile il sorgente JSP in un package di deploy. Questo un problema di sicurezza e licenze: anche se le pagine JSP contengono solo logica di presentazione, si tratta pur sempre di propriet intellettuale. Se il caso questo, si possono precompilare le pagine JSP e non dare il sorgente delle JSP ai clienti. Si dovr distribuire solo il bytecode, rendendo difficile la scoperta del codice sorgente. Una volta che le pagine JSP siano state compilate in file di classi Java si potrebbe anche "offuscarle" rendendo ancora pi difficile la lettura attraverso utilit di decompilazione. La precompilazione delle pagine JSP mette a disposizione pi opzioni per trattare simili problemi.
dome sempre, ci sono alcuni svantaggi nella precompilazione di pagine JSP. Da un lato si perde la possibilit di aggiornamento o di rimediare velocemente a un problema. Invece di porre semplicemente un nuovo file JSP nel server lasciando che il container lo compili al momento dell'accesso, si deve effettuare manualmente la ricompilazione e effettuare il deploy della classe servlet. Un altro svantaggio consiste nel fatto che, quando alcuni container rilevano un problema con una sola JSP, non ne elaborano pi altre. Quando ci accade, si deve accertare che ogni pagina JSP si compili correttamente prima che il container effettui il deploy della web application. Gli sviluppatori potrebbero considerarlo un vantaggio, ma in ambienti di produzione potrebbe rendere difficoltosa una correzione veloce di un bug. Sfortunatamente non esiste un modo standard di precompilare le pagine JSP. Ogni container ha un suo modo per svolgere quest'operazione. Di seguito si descrive brevemente la precompilazione effettuata da tre container: Tomcat 4.0. Resin 2.0 e WebLogic 7.0.
resin/bin/httpd -e <URL>
Con tale approccio, si pu compilare solo una JSP per volta, anche se non difficile creare uno script che compili l'intera web application. Nel prosieguo del capitolo verr discusso un altro modo molto pi facile che utilizza Ant.
le recenti specifiche descrivono ora un meccanismo che permette a queste classi di essere scaricate automaticamente ogni volta che venga fatta una request usando una interfaccia remota EJB. Nella maggior parte dei casi sufficiente porre il file JAR del container EJB nella directory WEB-lNF/lib. Per esempio, nel caso si stia usando WebLogic 6.0 o successivo, si pu mettere il file weblogic.jar nello strato web dal momento che contiene tutte le necessarie risorse lato client.
La Figura 16-2 mostra 11 sottodirectory, 8 delle quali contengono le JSP dell'applicazione. La directory images contiene immagini che sono utilizzate globalmente nell'applicazione, la directory stylesheets ne contiene i fogli stile e la directory include contiene file inclusi mediante include statici o dinamici. Il passo seguente sta nell'assicurarsi di avere una directory WEB-INF all'interno della directory root della propria web application. La directory WEB-INF contiene tutte le risorse usate internamente da un'applicazione. Per esempio, risiedono in questa directory i file TLD per le librerie dei tag personalizzati, come pure il descrittore di deploy o il file di configurazione di Strts. Nessuna risorsa all'interno di questa directory- pu essere resa pubblica esternamente all'applicazione. All'interno della directory WEB-INF si creitoo due sottodirectory: una chiamata classes e l'altra lib. La directory classes dovrebbe includere tutte le classi di utilit e servlet. La directory lib dovrebbe contenere invece i file JAR da cui dipende l'applicazione. Una volta che tutte le risorse per la web application sono state messe ciascuna nel posto giusto, si deve usare jar, il tool Java per l'archiviazione, per comprimere directory e file. Da linea di comando si entri nella directory- root della propria web application e si usi l'utilit jar. Si deve dare un'estensione .war il file nel seguente modo:
Dal momento che si passati alla directory root, la directory storefront non verr inclusa nel file WAR. Ci corretto, dal momento che una volta installata si vorr chiamare la directory root in un'altro modo. Quando si installa la web application, se si desidera decomprimere il file WAR, tutto quel che si deve fare creare una directory ed estrarre i file, in questo modo:
non va installato in una directory con un nome di path lungo, dal momento che il file batch adoperato per eseguire l'installazione potrebbe non gestirlo correttamente. Si consulti, per maggiori ragguagli, la documentazione di Ant.
<project name="storefront" default="war" basedir="."> The project is named storefront and the default target to execute is the war target. The default target is what gets
executed if you type ant at the command line without specifying a target. Because the project root directory is the same directory that the build.xml file is located in, "." is used to indicate the base directory property. The build directory structure for the Storefront application is shown in Figura 16-3.
Queste tre propriet definiscono dove sono situati il container delle servlet, la directory di deploy e le classi API delle servlet. Per prima cosa, la propriet webserver.home deve puntare alla directory root del container delle servlet. In questo caso, si usa Tomcat 4.0 come web server e servlet container. Il file build.xml di Storefront supporta molti altri container ma sono tutti commentati; si deve solo togliere il segno di commento a quello che si intende usare. La directory di deploy di Tomcat la directory webapps posta all'interno della directory root di Tomcat. Tomcat conserva le classi delle API delle servlet nel file common/lib/servlet.jar relativo alla directory root di Tomcat. In seguito, si deve definire il classpath che sar usato durante la compilazione della propria applicazione. Ant permette di associare un insieme di file con il nome di una propriet. Nel seguente frammento di build.xml, la lista di tutti i file JAR necessaria alla compilazione di Storefront e collegata alla proprietbuild.classpath:
<path id="build.classpath"> <pathelement location="${servlet.jar}"/> <pathelement location="${lib.dir}/commons-beanutils.jar"/> <pathelement location="${lib.dir}/commons-collections.jar"/> <pathelement location="${lib.dir}/commons-dbcp.jar"/> <pathelement location="${lib.dir}/commons-digester.jar"/> <pathelement location="${lib.dir}/commons-logging.jar"/> <pathelement location="${lib.dir}/commons-pool.jar"/> <pathelement location="${lib.dir}/commons-services.jar"/> <pathelement location="${lib.dir}/commons-validator.jar"/> <pathelement location="${lib.dir}/jdbc2_0-stdext.jar"/> <pathelement location="${lib.dir}/log4j.jar"/> <pathelement location="${lib.dir}/poolman.jar"/> <pathelement location="${lib.dir}/struts.jar"/> <pathelement location="${lib.dir}/tiles.jar"/> <pathelement path="${build.dir}"/> </path>
Potevano essere usate meno righe di elementi include per definire la propriet build.classpath; tuttavia molto pi chiaro elencare esplicitamente ogni file JAR usato durante il processo di compilazione per non aggiungere o omettere nulla che possa impedire un corretto svolgimento dell'operazione. La dereferenziazione del nome della propriet tramite la sintassi di Ant ${property} permette al task di impiegare la propriet build.classpath.
<target name="compile" depends="prepare"> <javac destdir="${build.dir}" deprecation="on"> <classpath refid="build.classpath"/> <src path="${src.dir}"/> </javac> </target>
Un target potrebbe dipendere da zero o pi target secondo la seguente sintassi:
<target name="prepare"> <tstamp/> <mkdir dir="${build.dir}"/> <mkdir dir="${dist.dir}/lib"/> </target> II target prepare genera valori di timestamp che possono essere convertiti in propriet e aggiunti ai prodotti della
compilazione come i file JAR e WAR. Per questo piccolo progetto, tuttavia, non si utilizzano timestamp. Il target prepare crea anche le sottodirectory necessarie per le classi Java e per i file WAR. Il target compile istruisce Ant affinch esegua il compilatore javac su tutti i file all'interno della directory con i sorgenti e a inviare tutti i file delle classi alla directory di compilazione. L'opzione deprecation attiva, per cui si otterr un messaggio dettagliato se si include un metodo deprecato in un file sorgente:
<src path="${src.dir}"/> </javac> </target> Il task javac impiega la propriet build.classpath illustrata nella sezione precedente.
<target name="war" depends="compile"> <echo>building war...</echo> <war warfile="${dist.dir}/lib/storefront.war" webxml="${web.dir}/WEB-INF/web.xml"> <fileset dir="${web.dir}"/> <classes dir="${build.dir}"/> <classes dir="${lib.dir}"> <include name="*.properties"/> <include name="poolman.xml"/> </classes> <lib dir="${lib.dir}"> <include name="*.jar"/> </lib> </war> </target>
Come visto in precedenza, il target war quello predefinito per il progetto. Questo significa che all'esecuzione di Ant da linea di comando senza un argomento target, viene eseguito il target war. Il target war viene eseguito solo se il target compile stato prima terminato con successo. Il task war richiede la definizione di un nome per il file WAR e la posizione del file web.xml. Tutti gli altri attributi sono facoltativi. Se si interessati a conoscerli, vi un elenco nella documentazione in linea di Ant (http://jakarta.apache.org/ant/manual/CoreTasks/war.html). Un'altra importante risorsa su Ant Ant: The Definitive Guide di Jesse Tilly e Eric Burke (O'Reilly). Gli elementi annidati informano il task war circa la collocazione dei contenuti del file WAR. L'elemento fileset definisce il contenuto web di base del file WAR. Questo elemento viene utilizzato per dichiarare dove sono collocati i file HTML, le JSP, le immagini e cos via. L'elemento classes punta ai file di classi Java che dovrebbero essere inclusi nella directory WEB-INF/classes del file WAR e l'elemento lib dichiara quali file debbano essere inclusi nella cartella WEBINF/lib. In Storefront, viene incluso tutto il contenuto della sottodirectory web. Le varie sottodirectory contengono tutte le risorse necessarie (HTML, JSP, immagini etc). Tutte le classi compilate nella sottodirectory build sono copiate nella directory del file WAR WEB-INF/classes insieme ai file di propriet. Tutti i JAR di terze parti nella sottodirectory lib sono copiati nella directory WEB-INF/lib del file WAR. Se la sottodirectorv lib contiene JAR che non si desidera includere, possibile usare il seguente codice:
Alcuni amministratori di sistema potrebbero disabilitare cron per risparmiare potenza di calcolo. In questo caso si dovranno ottenere i permessi per poter eseguire programmi cron. Si pu verificare che cron sia in esecuzione e che si abbiano i permessi di accesso, digitando crontab -l sulla linea di comando.
Si deve soltanto modificare crontabs e aggiungervi una riga che invochi ant che a sua volta far partire il processo di build. Si consultino le pagine del manuale Unix per maggiori informazioni circa l'uso di cron.
Non si ha un controllo cos granulare degli intervalli di tempo come nella piattaforma Unix. Tuttavia, uno strumento sufficiente per produrre distribuzioni giornaliere a un certo orario. Si veda la documentazione di Windows per dettagli pi approfonditi. Lo scaricamento e la compilazione dei sorgenti fanno parte dell'automazione del proprio ambiente. Molti sistemi di controllo dei sorgenti come Clear Case, CVS e PVCS hanno una API a linea di comando per estrarre file sorgenti in modalit di sola lettura per compilarli. Con Java e Ant, questa parte del processo di automazione resa piuttosto rapida.
CAPITOLO 17
Ottenere prestazioni adeguate per un'applicazione un fattore della massima importanza per gli utenti. Non molti di questi infatti potranno apprezzare un bel design e uno standard di programmazione all'avanguardia se l'applicazione lenta. Persino la migliore e pi utile delle applicazioni verr rifiutala dalla comunit degli utenti se risulta lenta nella risposta alle loro richieste. Nel caso delle applicazioni basate sul web, le aziende devono testare e misurare la velocit dei vari componenti web, quante richieste simultanee il sito possa gestire e quale sia la scalabilit del software e dell'hardware. Devono anche definire di quanto le prestazioni verranno degradate nei momenti di picco. Questo capitolo esplora le implicazioni legate alle prestazioni di Struts e delle tecnologie che lo affiancano per la costruzione di web application, e del modo in cui alcune decisioni legate alla progettazione e alla programmazione influiscano sulle prestazioni totali. Si distinguer tra test di prestazioni, test di carico e test di stress, esaminando i passi necessari a condurli.
Il problema che ogni applicazione divrsa e di conseguenza ha differenti requisiti non funzionali. Un'applicazione potrebbe necessitare di un tempo medio di risposta di 3.0 secondi e supportare 50 utenti contemporaneamente, mentre un'altra potrebbe dover supportare 500 utenti simultaneamente. Il test delle prestazioni un po' pi oscuro del test funzionale, per il quale facile capire quando l'applicazione svolge il proprio compito.
Secondo Alberto Savoia, il direttore del dipartimento Ricerca sul Software presso la Sun Microsystems, ci sono quattro leggi comportamentali che rendono fondamentali per il successo di un'azienda le prestazioni delle pagine web: Legge dell'abitudine Questa legge indica che gli utenti web sono abitudinari ma non fedeli. Se trovano un sito web che risponde ai propri bisogni, essi tendono a continuare a utilizzarlo. Se il sito inizia a rispondere con lentezza e l'utente deve aspettare, quest'ultimo cercher un'altro sito che corrisponda agli.stessi bisogni. Il punto che necessaria un forte impegno sulle prestazioni per far tornare ogni volta gli utenti. Legge della prospettiva dell'utente Questa legge stabilisce che si deve misurare la prestazione della propria applicazione dal punto di vista dell'utente e non dal proprio. Cio necessario considerare che magari noi operiamo all'interno di una rete a 100 MB non trafficata, ma l'utente potrebbe invece usare un modem e molta meno banda. Si tenga ben presente perci quali sono le capacit dell'ambiente e della rete dell'utente e si compiano test appropriati. Legge della responsabilit Questa legge stabilisce che a un utente non interessa la causa di una magra prestazione del sito; l'imputata sar sempre l'applicazione. Il problema potrebbe essere il suo provider o un'altra ragione che niente ha a che fare con la nostra applicazione, ma la maggior parte degli utenti non sar capace di isolare il problema a quel livello e a essere biasimate saranno applicazione e/o sito. Occorre essere consapevoli di tutti i fattori che potrebbero influenzare le prestazioni della nostra applicazione. Legge delle aspettative Questa legge indica che la soddisfazione dell'utente basata sulle proprie aspetta tive che sono in relazione con le personali esperienze legate ad altri siti web simili. Quando si misurano le prestazioni non ci si deve basare solo su numeri arbitrari e astratti ma si devono fare confronti con la concorrenza. Queste leggi semplici e basate sul buon senso spiegano il comportamento umano nei confronti delle prestazioni dei siti web. In generale, tuttavia, lento lento e veloce veloce, quindi si possono fare delle generalizzazioni tra applicazioni e domini di elaborazione. Ma prima di discutere come riconoscere se esiste un problema di prestazione in un'applicazione, deve essere fatta una distinzione tra i tipi di test da eseguire.
Gli utenti virtuali sono utenti simulati che le applicazioni di test impiegano per emulare l'impatto di molti utenti su di un sistema senza richiedere l'intervento di utenti reali. Per ciascun utente virtuale viene registrala una sessione utente reale che pu essere eseguita come se un utente reale stesse usando l'applicazione.
Anche se i test di stress, di prestazione e di carico sono correlati, differiscono tra loro sia sostanzialmente che operativamente. Il test delle prestazioni prevede l'esecuzione degli aspetti funzionali dell'applicazione e la misurazione del tempo necessario a ottenere un risultato completo. Il tempo impiegato da un solo task viene detto tempo di risposta. Se il metodo eseguito pi volte e si opera una media, il risultato conseguito il tempo medio di risposta. Il tempo medio di risposta per l'action signin di Storefront di circa 250 millisecondi.[3] Questo risultato vale per un utente singolo. Si dovrebbe sempre condurre il test iniziale di performance con un solo utente per ottenere una linea di base. Se esistono dei colli di bottiglia per un utente singolo si pu scommettere che gli stessi problemi avranno un impatto devastante quando cominciano a collegarsi decine di utenti. In generale, si pu dire che pi veloce il tempo di risposta pi veloce l'applicazione. Questo tempo si potrebbe anche definire come il tempo di transazione per la funzione in test.
[3]
I test sono stati condotti su di un Pentium III 750 MHz con I GB di memoria, che conteneva tutti i livelli.
Sulla base del tempo di risposta si pu arguire un tempo di throughput. Il throughput (o portata) definisce il numero di transazioni che avvengono in un certo lasso di tempo. Il throughput teorico che viene calcolato sulla base di un solo utente probabilmente varier con carichi reali. A seconda del rhultiprocessing e di altre particolarit hardware e software, le applicazioni potranno avere un throughput maggiore aggiungendo risorse hardware o software. In questo modo l'applicazione elaborer un numero maggiore di transazioni permettendo un aumento dei valori di throughput. Il test di carico analogo al test di volume. Questo tipo di test viene svolto per capire il modo in cui l'applicazione reagir a un carico molto pesante di utenti del sistema. Nel corso di questo tipo di test si potranno modificare le configurazioni hardware e software per determinare quale configurazione offre il miglior throughput per un certo carico di utenti. Il test di carico di solito difficoltoso da svolgere perch implica un costante impegno per trovare la miglior combinazione per il miglior throughput. Nessuna applicazione pu sostenere un carico infinito. L'idea consiste nel trovare un buon accordo tra tempo di risposta e numero massimo di utenti. Il throughput di solito misurato in transazioni per secondo (tps), ma pu essere anche misurato in transazioni per minuto, o ora e cos via. Tramite valori di risposta e valori di throughput si possono prendere le decisioni pi opportune circa la migliore configurazione della propria applicazione allo scopai di assicurare prestazioni elevate e scalabilit per gli utenti. Questi valori andrebbero trasmessi ai tecnici di rete per capire quanta larghezza di banda richiede l'applicazione. Il test di stress la successiva logica estensione. Il test di stress essenzialmente un test di carico tramite l'utilizzo di carichi di picco. Se si svolge questo test, l'intento di mettere alla prova l'applicazione ai suoi limiti, per osservarne le reazioni, l'uso della memoria e qualsiasi altro problema si possa delineare. Spingere la propria applicazione ai limiti sotto un carico simulato pesantissimo offre molti benefici, tra i quali: Permette di identificare colli di bottiglia nell'applicazione sotto un forte carico di utenti prima dell'introduzione nell'ambiente di produzione. Permette il controllo di costi e rischi prevedendo limiti di scalabilit e prestazioni. Incrementa disponibilit e tempi di caricamento tramite una corretta pianificazione delle risorse. Evita dei ritardi dovuti a problemi inaspettati di performance o scalabilit. I test di performance, carico e stress dovrebbero essere svolti tutti per ogni applicazione per poter avere un quadro completo della situazione. Possono essere diretti a parti dell'applicazione che potrebbero rivelarsi colli di bottiglia sia sotto carichi normali sia all'aumentare del numero di utenti.
Organizzazione Apache Group Mercury Interactive Rational RadView Empirix Segue Software, Inc. Microsoft Apache Group SourceForge
Prodotto JMeter LoadRunner SiteLoad WebLoad e-Test Suite SilkPerformer WAS Flood The Grinder
URL http://jakarta.apache.org/jmeter/ http://www-svca.mercuryinteractive.com http://www.rational.com http://www.radview.com http://www.empirix.com http://www.segue.com http://webtool.rte.microsoft.com http://httpd.apache.org/test/flood/ http://grinder.sourceforge.net
Storefront rappresenta un'applicazione tipica di commercio elettronico che potreste avere gi visto su Internet o anche avere gi sviluppato. Una normale applicazione di questo tipo si connette a un database con decine o centinaia di migliaia di campi. Nel nostro caso. Storefront usa un'implementazione di debug e, volutamente, non si connette a un database, in modo che non sia necessario un database installato solo per eseguire l'applicazione di esempio. Non c' motivo di effettuare un test completo di Storefront: i numeri non avrebbero troppo significato. utile per mostrare come iniziare e i passi da seguire per avere delle misurazioni di prestazione su un'applicazione. Ecco i passi fondamentali: 1. 2. 3. 4. 5. 6. Identificare gli obiettivi di prestazione ricercati. Stabilire le basi per la misurazione delle prestazioni. Eseguire test per raccogliere dati. Analizzare i dati per scoprire dove sono i problemi. Operare le necessarie modifiche al software e all'hardware per incrementare la prestazione. Ripetere i punti da 3 a 5 secondo necessit per raggiungere gli obiettivi preposti.
Si tratteranno di seguito tutti questi punti. Per questo esercizio si user una versione in scala ridotta di Mercury Interactive LoadRurmer, chiamato Astra LoadTest. Si tratta di un prodotto commerciale ricco di caratteristiche. Si pu scaricare una versione dimostrativa che supporta fino a 10 utenti Contemporanei dal sito http://wwwsvca.mercuryinteractive.com/products/downloads.html.
Figura 17-1. La schermata di registrazione dei risultali nel programma Astra LoadTest
Con Astra, e con altri strumenti analoghi, ogni interazione con la web application pu essere registrata come transazione separata. Nella Figura 17-1, ogni elemento nell'albero sulla sinistra dello schermo rappresenta una transazione separata che pu essere ripetuta e di cui possono essere misurati i parametri di prestazione. Una volta iniziata la registrazione dei risultati, tutte le interazioni tra client e server sono salvate, inclusi i parametri di richiesta e gli header. Si pu poi ripetere questa registrazione modificando diversi parametri quali, ad esempio, il numero di utenti che partecipa all'operazione. Quindi, ottenuti gli script appropriati, si pu stabilire il riferimento, che di solito dato dalla misurazione su un singolo utente che usa l'applicazione. Il numero di utenti virtuali pu variare a seconda che si stia misurando la prestazione o il carico. Solitamente meglio partire con un singolo utente e in seguito incrementarne il numero. Se l'applicazione lenta con un solo utente, sar presumibilmente lenta anche con pi utenti. La Figura 17-2 mostra gli script di test della Figura 17-1 in azione sull'applicazione Storefront con un singolo utente.
Concluso l'insieme di test, il programma presenta un sommario dei risultati del test per la propria applicazione. La Figura 17-3 mostra i punti di riferimento per Storefront con un singolo utente.
Ottenuti i punti di riferimento, se si deduce che le prestazioni sono da aumentare si pu modificare la propria applicazione. Sfortunatamente non quasi mai un'operazione semplice. Si devono determinare i punti deboli su cui concentrarsi. Non c' motivo di velocizzare l'applicazione in punti dove gi veloce. Si devono utilizzare gli strumenti adatti a determinare le posizioni dei colli di bottiglia.
L'ottimizzazione di un'applicazione un'operazione simile al debug. Si vede il punto in cui un'applicazione impiega pi tempo per l'elaborazione, il modo in cui sono effettuate le chiamate a una singola funzione, quanti oggetti vengono creati, e via dicendo. Si parte da un livello alto per discendere fino al livello dei metodi che causano problemi di prestazioni o scalabilit. Una volta posto rimedio, si eseguono nuovamente i test. Questo processo viene ripetuto finch i problemi non sono tutti risolti o finch non si deve rendere disponibile il prodotto.
Gli strumenti per misurare le prestazioni possono dare comunque un aiuto per determinare le aree pi problematiche. La Figura 17-4, ad esempio, illustra come il tempo medio di risposta per transazione di una certa action sembri maggiore delle altre.
Le misurazioni qui riportate sono straordinariamente buone. Il tempo peggiore di risposta per Storefront di 0.25 secondi. La maggior pane degli sviluppatori sarebbe disposta a uccidere per avere problemi come questo! Ma in questa applicazione non viene svolto nessuna elaborazione reale, perch solo un esempio in cui non ci si connette a un database con centinaia di migliaia di record tra cui cercare. Ci, tuttavia ci fa notare un fatto: solo perch una transazione pi lenta delle altre non vuol dire che sia lenta tout court. Potrebbe solo essere lenta relativamente ad altre transazioni davvero velocissime. Non si sprechi tempo per velocizzare qualcosa che gi veloce. Ci si deve concentrare piuttosto sulle parti che sono davvero lente. 0.25 secondi vuol dire molto veloce e, se l'applicazione fosse reale, potremmo subito distribuirla. L'operazione che mostra il peggior tempo di risposta nella Figura 17-4 l'action "view item detail". Con Astra, si pu osservare la transazione ancora pi nei particolari. La Figura 17-5 analizza la pagina di visione dei dettagli articolo, suddividendola nelle parti che la compongono.
Figura 17-5. La pagina di visione dei dati di un articolo scomposta nelle sue parti
Cos, si pu spostare l'attenzione sui tempi di download delle varie pagine per capire come si comporta al confronto la pagina "view item detail" (Figura 17-6).
Come si potuto notare, riconoscere problemi di prestazione necessita di un po' di lavoro da detective. Si deve sempre guardare dietro ogni angolo e sotto ogni pietra.
Non si deve dimenticare che, oltre alle sessioni utente, ci sono altre risorse che richiedono memoria. Esistono i dati con scope di applicazione, il resto del framework Strus, i componenti propri dell'applica ione, il container stesso, la JVM.e via dicendo. Sono tutti fattori da tenere in z considerazione.
Come si evince facilmente, l'utilizzo della sessione per l'immagazzinamento di dati pu fare esaurire rapidamente la memoria. Un'alternativa da considerare potrebbe essere HttpServletReques per conservare temporaneamente dei dati che possano essere impiegati da altri componenti e quindi richiamati dal garbage collector al completamento della richiesta. La responsabilit della distruzione dei dati, con dati che hanno lo scope di richiesta, suddivisa tra container e JVM ma non dell'applicazione.
La JVM supporta molte diverse opzioni per l'impostazione e la configurazione dei propri parametri di esecuzione. A volte potrebbe essere utile modificare queste informazioni per ottenere una migliore prestazione. Le due opzioni pi importanti ai fini di prestazioni e scalabilit sono Xms(size)e -Xmx(size). L'opzione -Xms permette di impostare la misura iniziale dell'heap dell'applicazione mentre -Xmx permette di impostare la dimensione massima dell'heap. L'heap l'area di immagazzinamento della memoria per l'applicazione. Pi grande l'area di immagazzinamento, pi la memoria disponibile per l'applicazione. Ci si potrebbe chiedere, "e perch non impostarla allora alla dimensione massima consentita dalla memoria fisica?". Il problema che si tratta comunque di un'area che il garbage collector deve ripulire. Il garbage collector della JVM va periodicamente in esecuzione e tenta di recuperare tutta la memoria inutilizzata. Il garbage collector deve passare attraverso tutta la memoria assegnata a un'applicazione. Ogni volta che il garbage collector si attiva, l'applicazione viene momentaneamente bloccata. Pi tempo questo impiega a svolgere la propria funzione, pi gli utenti staranno in attesa. L'impostazione corretta della dimensione dell'heap quindi molto importante e non facile determinarla in generale per una qualsiasi applicazione. Ogni applicazione diversa e crea e distrugge oggetti con frequenza diversa. Si possono per impostare valori standard di riferimento ed effettuare cambiamenti progressivi. Si raggiunger prima o poi un punto in cui scalabilit e prestazioni peggiorano all'aumentare dei valori. Si ritorner allora ai valori immediatamente pi bassi che saranno quelli ottimali. In generale si dovrebbero utilizzare i seguenti valori di partenza:
possono anche migliorare le prestazioni delle applicazioni che usano riferimenti remoti per mezzo di un meccanismo di caching dei riferimenti remoti. Per maggiori dettagli, si veda il Capitolo 13.
Appendici
Appendice A. Cambiamenti rispetto a Struts 1.0
Con il passaggio dalla versione 1.0 alla 1.1, al framework Struts sono stati apportati numerosi cambiamenti: di essi si parlato nel corso di tutto il libro, ma in questa appendice si riassumono tutte le variazioni, illustrando inoltre le caratteristiche pi importanti tra quelle nuove.
A.4.3 Plug-ins
La caratteristica PlugIn stata aggiunta a Struts affinch disponga di un meccanismo che notifichi e inizializzi i servizi all'avvio e alla conclusione dell applicazione Struts. C' un'ampia gamma di possibilit per questa caratteristica e, infatti, il Validator e la libreria Tiles traggono entrambi dei vantaggi dal meccanismo PlugIn. Si veda il Capitolo 9 per maggiori informazioni sull'uso dei plug-in nelle applicazioni.
Una volta scaricata la versione pi adatta, si estrae il contenuto dell'archivio all'interno di una directory, che non deve essere necessariamente vuota, anche se molto meglio che lo sia: il file di distribuzione di Struts sotto forma di codice sorgente scompatta tutto il suo contenuto all'interno di una sottodirectory denominata jakarta-struts. Prima di poter effettuare la compilazione dei file sorgenti, sar necessario anche scaricare alcuni pacchetti software necessari al buon funzionamento di tutto (sempre che gi non siano stati installati precedentemente). L'elenco seguente descrive il software richiesto: Java SDK Versione 1.2 o pi recente. Si pu scaricare da http://java.sun.com/j2se/. Servlet container Un container per servlet che supporti le specifiche Servlet 2.2 o successive e le specifiche JavaServer Pages 1.1 o successive. Pool di compilazione Ant Versione 1.3 o successiva. Si pu scaricare da http://jakarta.apache.org/ant/. Classi Servlet API Tutti i servlet container dovrebbero contenere i file necessari; occorre solo garantire che i JAR necessari si trovino nel classpath per il tool di compilazione. Parser XML Deve supportare JAXP versione 1.0 o successive. Il parser Xerces compatibile e pu essere scaricato da http://xml.apache.org/xerces-j/. Xalan XSLT Processor Versione 1.2 o successiva, sebbene occorra notare che sono stati riferiti alcuni problemi con la versione 2.0. Questo programma di elaborazione utilizzato per convertire i file XML di documentazione in file HTML. Xalan include anche il parser Xerces, ed disponibile all'indirizzo http://xml.apache.org/xalan-j/index.html. Classi del package Optional JDBC 2.0 Il framework Struts supporta un'implementazione di javax.sql.DataSource e richiede quindi la presenza delle classi JDBC 2.0 Optional, le quali possono essere scaricate dall'indirizzo http://java.sun.com/products/jdbc/download.html. Sar anche necessario scaricare i package di terze parti elencati in Tabella B-l ed estrarre i file JAR in essi inclusi. Ciascun package disponibile in distribuzione sia sorgente che binaria e, a meno di non essere veri masochisti, meglio scaricare quella binaria.
Nome package Commons BeanUtils Commons Collections Commons Digester Commons Logging Commons Pool Commons Services Commons DBCP Commons Validator Jakarta ORO
URL http://jakarta.apache.org/commons/beanutils.html http://jakarta.apache.org/commons/collections.html http://jakarta.apache.org/commons/digester.html http://jakarta.apache.org/commons/logging.html http://jakarta.apache.org/commons/pool/index.html http://jakarta.apache.org/builds/jakarta-commons/nightly/ http://cvs.apache.org/viewcvs/jakarta-commons/dbcp/ http://cvs.apache.org/viewcvs/jakarta-commons/validator/ http://jakarta.apache.org/oro/
L'indirizzo http://jakarta.apache.org/commom/components.html fornisce un ottimo quadro riassuntivo sulle date a partire dalle quali saranno disponibili le varie release dei componenti Commons.
I file JAR estratti dalle distribuzioni binarie di questi package vanno collocati in maniera che risulti facile effettuare i riferimenti ad essi, dal momento che serviranno per il passo successivo. Ciascun package viene estratto all'interno della sua sottodirectory ed possibile lasciarli in tale posizione per avere riferimenti pi facili. Il passo successivo sta nel creare un file build.properties che specifichi le relazioni di ambiente, quali la collocazione dei JAR terze parti estratti nella fase precedente. La distribuzione sorgente di Struts fornisce un file build.properties.sample nella directory top-level della distribuzione: basta aprire questo file e rinominarlo build.properties. Sar necessario personalizzare questo file per le peculiarit del proprio ambiente. I patii del file sono relativi alla collocazione del file di propriet: per esempio, al fine effettuare il riferimento al file JAR Commons BeanUtils che si trova in una directory lib a un livello immediatamente superiore, sar necessario modificare in questo modo il file build.properties:
# The JAR file containing version 1.0 (or later) of the Beanutils package # from the Jakarta Commons project. commons-beanutils.jar=../lib/commons-beanutils-1.3/commons-beanutils.jar
Una volta messi a posto tutti i path per i file nel build.properties, arrivato il momento di trasformare i file sorgenti di Struts in file binari. Occorre che Ant sia installato e configurato in maniera corretta e che il file xalan.jar sia stato scaricato e posto nella directory <ANT_HOME>/lib, affinche un task di Ant possa elaborare i file XML di documentazione. Sar poi necessario essere in grado di eseguire Ant da linea di comando nella directory top-level della distribuzione sorgente. Completati tutti questi punti, si va alla directory in cui collocato il file build.properties e si digita:
ant dist
Nella console saranno prodotti dei messaggi. Quando, alla fine, termineranno, dovrebbero indicare che la compilazione andata a buon fine, come mostrato in Figura B-l.
Figura B-1. Quando l'operazione di build completata, viene presentato un messaggio di successo
Se la build non va a buon fine, saranno prodotti dei messaggi di errore, di solito con informazioni sufficienti a determinare la causa dei problemi. Per esempio, se ci si dimentica di collocare il file xalan.jar nella directory <ANT_HOME>/lib, sar prodotto qualcosa di simile a quanto mostrato in Figura B-2.
Figura B-2. Quando ci sono errori di build, viene in genere fornito un registro dei problemi
Nel caso in cui la compilazione sia andata a buon fine, i file binari e dipendenti di Struts vengono copiati nella directory dist sotto quella top-level.
org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
7. Creare un resource bundle di Struts e codiarlo nella directory WEB-INF/classes. Si veda il Capitolo 12 per i dettagli sulla formattazione di questo bundle e il Capitolo 4 per sapere come configurare il file di configurazione di Struts affinch utilizzi questo bundle.
Non c' altro da fare. Per quanto riguarda l'installazione di Struts, questo quanto. Sar necessario costruire le necessarie pagine JSP e altri componenti del framework, quali le classi Action e ActionForm, ma quanto esposto tutto ci di cui c' necessit per far funzionare l'ambiente operativo. Un consiglio da seguire sicuramente quello di fare pratica con le applicazioni di esempio distribuite con Struts. Si tratta di web application complete, rilasciate sotto forma di package WAR che possibile copiare nella directory webapps del container. Per far funzionare Struts possibile utilizzare uno tra i vari container esistenti. In molti casi, le fasi dell'installazione appena esposte possono essere seguite alla lettera; tuttavia, non tutti i container sono identici e alcuni hanno bisogno di qualche aggiustamento rispetto a quanto elencato: tali modifiche, quando necessarie, sono esposte di seguito.
Appendice C. Risorse
Questa appendice fornisce un elenco di risorse utilizzabili per l'approfondimento dell'argomento Struts.
Colophon
Our look is the result of reader comments, our own experimentation, and feedback from distribution channels. Distinctive covers complement our distinctive approach to technical topics, breathing personality and life into potentially dry subjects. The animal on the cover of Programming Jakarta Struts is a Percheron draft horse. This breed originated in the province of Le Perche in northwestern France. Purebreds are predominantly black or gray, and some have white markings on their heads and feet. They weigh an average of 2000 pounds and are usually 16 to 17 hands (64 to 68 inches) high. Percherons adapt well to many climates and are extremely versatile: their ruggedness and power makes them ideal for hauling heavy loads, their placid nature makes them easy to handle, and their natural grace and beauty complement the finest horsedrawn carriages. They can be ridden, and some have even been made into jumpers. In 732 A.D., Arabian horses abandoned by the Moors after the Battle of Tours were bred with native Flemish stock, producing the first Percherons. When the Crusaders invaded ten centuries later, more Arabian blood was added to the breed. However, the number of Percherons dwindled during the French Revolution, as horse breeding was suppressed. After the revolution, the new French government revived the breed by establishing a stud program for army mounts, using two Arabian sires at Le Pin, Normandy. In 1832 a foal named Jean Le Blanc was born in Le Perche, and all current Percheron bloodlines trace directly back to this horse. Le Perche has since exported purebred stock worldwide, and an official Breed Association registers Percherons to ensure that the line remains genetically pure. The breed was most popular after World War I, when farmers from both Britain and the United States became familiar with them while serving in the armed forces. In 1930, a U.S. census showed that registered Percherons outnumbered other draft horses by a margin of three to one, but after World War II, the farm tractor nearly replaced the breed entirely. However, it was kept alive by many farmers, especially those in Amish communities. Today, Percherons continue to work on farms and often perform in competition at livestock fairs. They are also used used to provide recreational hay, sleigh, and carriage rides. Emily Quill was the production editor and proofreader for Programming Jakarta Struts. Rachel Wheeler was the copyeditor . Jane Ellin and Linley Dolby provided quality control. Genevieve d'Entremont, Andrew Savikas, and Judy Hoer provided production assistance. Julie Hawks wrote the index. Emma Colby designed the cover of this book, based on a series design by Edie Freedman. The cover image is a 19thcentury engraving from the Dover Pictorial Archive. Emma Colby produced the cover layout with QuarkXPress 4.1 using Adobe's ITC Garamond font. David Futato designed the interior layout. This book was converted to FrameMaker 5.5.6 with a format conversion tool created by Erik Ray, Jason McIntosh, Neil Walls, and Mike Sierra that uses Perl and XML technologies. The text font is Linotype Birka; the heading font is Adobe Myriad Condensed; and the code font is LucasFont's TheSans Mono Condensed. The illustrations that appear in the book were produced by Robert Romano and Jessamyn Read using Macromedia FreeHand 9 and Adobe Photoshop 6. The tip and warning icons were drawn by Christopher Bing. This colophon was written by Phil Dangler. The online edition of this book was created by the Safari production group (John Chodacki, Becki Maisch, and Madeleine Newell) using a set of Frame-to-XML conversion and cleanup tools written and maintained by Erik Ray, Benn Salter, John Chodacki, and Jeff Liggett.