MokaPackages
è il framework configurabile in XML per la realizzazione
di applicazioni web secondo il pattern MVC. Nato qualche anno
fa come kernel minimale da utilizzare per lo sviluppo di applicazioni
web secondo il modello MVC, si è sviluppato nel tempo
fino a divenire uno strumento molto utile e completo.
Nel tempo ho seguito da vicino questo progetto sviluppandone
direttamente alcune delle parti più importanti con
il valido supporto dei collaboratori del team di MokaByte
(un grazie particolare ad Andrea Romagnoli particolare utile
per lo sviluppo di alcune parti quando ancora Struts non esisteva
e MVC era solo un pattern).
La filosofia che sta alla base di questo sistema è
la estrema semplicità con la quale e' possibile scrivere
e configurare una applicazione web tramite l'uso di XML.
Fin da quando il progetto ha iniziato ad assumere la giustificazione
di framework, ho nutrito il desiderio di dare maggior spazio
a questo che per certi versi è un figlio, fratello
del progetto MokaByte che resta il primogenito.
Il
modulo centrale di MokaPackages fu pensato e realizzato prima
dell'avvento di altri sistemi come Struts o WebWork, quando
ancora non esistevano sistemi che permettessero la realizzazione
di applicazioni web in modo semplice e personalizzabile tramite
XML. Proprio la nascita e la diffusione di Struts e di altri
sistemi molto famosi, mi ha fatto per lungo tempo dubitare
sulla utilità del portare avanti questo progetto e
sopratutto nella presentazione al grande pubblico. Struts
ha alle spalle un team di sviluppatori di tutto rispetto e
la popolarità che ha ormai acquisito è tale
che ogni ipotesi di competizione rischia di essere semplicemente
una fantasiosa utopia o un programmato suicidio imprenditoriale.
Perché
un altro framework MVC?
Se è vero che MokaByte nel 1996 ha anticipato i tempi
con la creazione della rivista web, è anche vero che
pubblicare adesso un framework per la realizzazione di applicazioni
web in MVC potrebbe suonare certamente anacronistico, in un
momento in cui ci sono altri framework validi e universalmente
accettati data la loro diffusione e rinomata validità.
Perché portare avanti questa idea e perché proporla
al grande pubblico ora che Struts ha di fatto monopolizzato
il mercato?
Dopo
aver disquisito a lungo sulla convenienza di abbandonare il
progetto o di proseguire in questa folle sfida, ci siamo convinti
che, sebbene sia più sintetico rispetto a Struts, il
nostro framework avesse delle potenzialità tali da
indurci nel proseguire questa nostra iniziativa. Ma oltre
alla bontà del prodotto (che in effetti è tutta
da dimostrare agli occhi del lettore), l'obiettivo che qui
ci prefiggiamo non è semplicemente quello di presentare
un prodotto software, ma anche di dar vita ad una comunità
di programmatori open source italiana.
MokaPackages verrà infatti presto rilasciato con licenza
open source in modo da coinvolgere il maggior numero di persone
per il suo sviluppo e crescita.
Visto l'interesse che ruota intorno alla rivista ed al numero
di iscritti nella community, crediamo che la nascita di un
gruppo di sviluppo open nel nostro paese sia una operazione
troppo importante per non essere tentata.
Infine una considerazione non di poco conto: MokaPackages
è un progetto italiano, con documentazione in italiano,
applicazioni di esempio in italiano e supporto online in italiano.
Non
mi è mai piaciuto sbilanciarmi in affermazioni forti
o valutazioni categoriche, ma ovviamente se devo difendere
la bontà della mia idea dovrò per forza cercare
di espormi. Spero che i lettori non valutino l'iniziativa
come un presuntuoso slancio di mania di grandezza del nostro
gruppo.
Vediamo
di dare un'idea del perché MokaPackages dovrebbe essere
adottato o semplicemente valutato.
La lunga attività di articolista, di consulente, di
docente per corsi Java (ed anche su Struts ovviamente), nonché
di programmatore mi ha portato ad avere una buona esperienza
nella valutazione di cosa sia utile e cosa non lo sia nello
sviluppo di applicazioni web di tutti i giorni.
Questa esperienza è risultata particolarmente utile
nella realizzazione di corsi, di libri (il nostro "Manuale
Pratico di Java" ha il riconosciuto pregio di presentare
tutto e solo il necessario per imparare in modo semplice ed
efficace quello che serve per iniziare a sviluppare applicazioni
professionali) e si è concretizzata anche nello sviluppo
del framework.
Credo di poter dire senza falsità che il nostro sistema
include al suo interno una serie di funzionalità che
mancano altrove mantenendo la sufficiente semplicità
necessaria a chi si avvicina a questo mondo per la prima volta.
Semplicità di configurazione e d'uso è quindi
la parola d'ordine che ci ha spinto in tutto questo tempo.
E' ovvio che questo si è pagato in termini potenza
espressiva, per la quale si sarebbe richiesto lo sviluppo
di un clone di Struts, operazione questa di sicuro insuccesso.
Dal
mio modesto punto di vista Struts pur essendo un sistema molto
potente, estremamente ricco di componenti e funzioni, manca
di alcune caratteristiche base che sono necessarie nella maggior
parte dei casi.
La filosofia open source basa le sue fondamenta sul concetto
che dice "tutto quello che non c'e' me lo posso scrivere
a mano", ma un buon progetto open dovrebbe fornire una
implementazione completa e facile da gestire delle funzionalità
di base e più usate nella maggior parte delle applicazioni,
lasciando al programmatore la estensione di quelle parti specifiche
del progetto o del cliente.
Tanto per fare un esempio valuto del tutto carente la gestione
dei ruoli utente, così come ritengo grave la mancanza
di un sistema automatico di inizializzazione della applicazione
web: sono queste alcune delle funzionalità necessarie
nel 99% dei casi e per questo ritengo la loro limitatezza
una mancanza non da poco.
Mokapackages
invece parte da una impostazione completamente differente:
costruito mattone dopo mattone in modo semplice limitando
l'attenzione per prima cosa ai pezzi più comuni, offre
la possibilità di crescere fino a diventare un framework
completamente personalizzabile in ogni parte in modo modulare
e scalare.
Ecco un breve elenco delle funzionalità già
presenti:
- Modello
MVC configurabile tramite file XML che associano viste e
moduli di business logic in modo astratto
- Possibilità
di passare parametri di configurazione alle action tramite
il file di configurazione XML.
- Modello
sorgente ascoltatore fra azioni (se eseguo la loginAction
vorrei in automatico scatenare altre operazioni di logica)
configurabile in XML.
- Disponibilità
di alcuni moduli di default (il login generico); questa
parte in particolare permetterà al sistema di crescere
senza alterare troppo la filosofia di base
- Completa
gestione dei ruoli utente su azioni e su viste
- Completo
sistema automatico di inizializzazione basato su componenti
(java beans) configurabili tramite XML (con dei semplici
tag posso dire quali componenti voglio inizializzare allo
start della applicazione o alla creazione di nuove sessioni)
- Una
sintetica ma completa custom tag library. Questa parte potrà
crescere ancora, anche se i tag più importanti ci
sono già tutti.
- Gestione
dei messaggi di errore tramite bundle: questa funzione è
praticamente una copia di quanto avviene in Struts
- Predisposizione
per l'internazionalizzazione
- Predisposizione
per la validazione
- Meccanismo
di comunicazione inter-web application
- Gerarchie
fra ruoli di autenticazione
Gli
ultimi quattro punti non sono ancora inseriti nella attuale
release del framework, e dovrebbero essere inseriti con la
prossima versione essendo in questo momento in fase di test.
Non elencheremo qui tutte le cose che differiscono rispetto
a Struts dato che ci vorrebbe un libro intero. Alcune delle
parti che al momento sono mancanti o per precise scelte progettuali
(es. la possibilità di definire sotto applicazioni
in strus-config.xml riteniamo che possa essere solo spunto
per una maggiore confusione; lo stesso risultato si può
ottenere con un altri strumenti più semplici), oppure
perché in questo momento sono fase di test finale di
prerilascio.
In
questo articolo presentiamo il framework nelle sue componenti
principali, con lo scopo di consentire a tutti di iniziare
a sviluppare applicazioni. Prossimamente il progetto verrà
rilasciato con sorgenti in licenza Open Source. Chiunque vorrà
potrà studiarne il funzionamento dall'interno.
I
componenti di MokaPackages
Secondo la terminologia di base del pattern MVC il sistema
è composto essenzialmente da tre componenti: il controller,
il model e la view. Senza entrare nel dettaglio dei vantaggi
e della filosofia di base di MVC (vedi [MVC]) si procederà
qui alla analisi dei vari componenti che lo costituiscono.
Nota:
per gli esempi qui riportati si tenga presente che il package
com.mokabyte.mvc fa riferimento a classi inserite direttamente
nel framework, mentre il com.mokabyte.testmvc si riferisce
a classi inserite nell'applicazione di esempio TestMVC allegata
all'articolo.
Il
model
Le operazioni di business logic fanno parte del cosiddetto
model (M di MVC): in MokaPackages il model è costituito
da una serie di classi Java che implementano una interfaccia
particolare, la Action e che sono associate a URL pattern
del tipo *.run. Ogni link del tipo *.run verrà associato
ad una particolare classe tramite quanto definito nel file
actions.xml; questo file ha una struttura simile all'altro
(routing.xml usato per mappare i nomi sulle viste), ma è
più articolato dato che consente di specificare più
informazioni per ogni singola azione. Ecco un esempio
<actions>
<action NAME="logout.run" CLASS="com.mokabyte.mvc.actions.commons.LogoutAction">
<executed PAGE="logout.jsp"
FORWARD="true"/>
</action>
<action
NAME="login.run" CLASS="com.mokabyte.mvc.actions.commons.PluggedLoginAction">
<executed PAGE="login-ok.show"/>
<failed PAGE="login-ko.show"/>
<init-params>
<init-param>
<param-name>user-authenticator-classname</param-name>
<param-value>com.mokabyte.mvc.users.DefaultUserAuthenticator</param-value>
</init-param>
<init-param>
<param-name>temp-dir</param-name>
<param-value>c:\\temp</param-value>
</init-param>
</init-params>
</action>
</actions>
Dato
che ogni azione svolge il suo lavoro restituendo una risposta
binaria, i tag <executed> e <failed> definiscono
i routing associati alla risposta della action.
Ad esempio la comune operazione di login può avere
successo oppure no.
In questo senso ad ogni azione viene associata alla risposta
positiva un routing ed un altro a quella negativa: la prima
si associa al mapping executed-routing l'altra al mapping
failed-routing.
Il routing potrà essere il nome di una vista (se login
va bene si mostra la vista login-ok.show che a sua volta sarà
mappata su loginOk.jsp) oppure di una altra azione (in tal
caso il risultato è l'esecuzione di due azioni in cascata).
Questo meccanismo per quanto possa apparire complesso o stranamente
articolato permette di svincolare totalmente il codice Java
da quello che accade dentro la classe da quello che accade
fuori (quale routing verrà eseguito) portando ad una
completa riutilizzabilità della action (si veda l'esempio
della RoledAction nell'esempio allegato).
Struts da questo punto vista ha una gestione differente, che
personalmente non condivido: all'interno della action si deve
decidere quale routing eseguire (utilizzando un nome che viene
mappato nel file di configurazione di Struts). In questo modo
le cose sono un po' più semplici, ma si lega il codice
Java della azione al nome che viene definito in XML. In questo
modo (sebbene una oculata gestione dell'XML e delle azioni
porta allo stesso risultato di MokaPackages) si corre il rischio
di non avere disaccoppiamento fra codice di business logic
(le azioni) e contesto di configurazione. L'azione di login
ad esempio è quasi sempre presente in una web application,
ma di volta in volta può essere associata a mapping
differenti.
Per
aggiungere una funzionalità di logica alla applicazione
web sarà quindi sufficiente creare una nuova action
e ridefinirne il metodo perform() che ha la seguente firma:
public
Routing perform(HttpServlet httpServlet, HttpServletRequest
httpServletRequest,
HttpServletResponse httpServletResponse) throws ActionException
Il
controller, a seconda del tipo di URL invocato, ricava la
azione corretta da eseguire la esegue e restituisce un risultato
che poi verrà utilizzato per mostrare una determinata
vista all'utente. Ad esempio il login può essere eseguito
tramite la LoginAction all'interno della quale nel metodo
perform() si effettuerà un accesso a database o altro
sistema di persistenza per controllare le credenziali d'accesso.
Le
switch action
Il fatto che le azioni abbiano risposte binarie implica, come
si è potuto vedere, il benefico vantaggio di poter
disaccoppiare del tutto il codice Java dal contesto di esecuzione
(che in fondo è la filosofia di MVC). Si pone però
il problema di gestire quei casi in cui una azione debba fornire
risposte multiple in funzione dell'input fornito.
Questo problema è stato qui risolto utilizzando una
particolare tipo di azione, la switch action che esegue in
cascata n azioni: la prima azione che restituisce true (corrispondente
al mapping executed-routing) come risultato fornisce il routine
da eseguire.
Ecco un esempio di configurazione di una switch action:
<switchAction
NAME="switch-delivery" CLASS="com.mokabyte.mvc.actions.DefaultAction">
<switchCase>item-cc</switchCase>
<switchCase>item-ct</switchCase>
<defaultCode PAGE="select-delivery-type.show"
FORWARD="true"/>
<executeAll>false</executeAll>
</switchAction>
Nel
caso specifico questa azione è presente nel nostro
negozio online e serve per decidere il tipo di operazione
da eseguire in funzione del tipo di spedizione scelta per
la consegna di un libro comprato.
I codici <switchCase> devono corrispondere a codici
di azioni precedentemente definite, altrimenti verrà
generato una eccezione.
Nell'esempio riportato il link switch-delivery provoca l'esecuzione
delle azioni associate ai codici item-cc e item-ct.
La prima azione che restituisce true fornirà il routing
di risposta. Se il flag executeAll è impostato a true
allora verranno eseguite tutte le azioni: se più di
una azione restituisse true allora verrà generata una
eccezione.
La
classe associata ad una switch action non svolge nessuna operazione
particolare: la sua presenza è necessaria esclusivamente
per permettere la creazione del costrutto switch. Per questo
si può genericamente utilizzarne una di default, la
SwitchActionImpl.
Le
fire actions
Nel caso in cui si vogliano eseguire n azioni in cascata all'esecuzione
di una determinata azione di base si possono usare le fire
actions. Ad esempio se in conseguenza dell'operazione di login
si volesse anche eseguire una operazione di notifica verso
il sistema dell'accesso di un utente (ad esempio per gestire
un sistema di log sugli accessi) si potrò usare questo
sistema: la LoginAction diventa una fire action alla quale
si possono associare n azioni listeners che verranno eseguite
in cascata dopo la login. Il meccanismo è quello tipico
del modello sorgente ascoltatore, anche se in questa particolare
implementazione l'esecuzione delle classi in cascata è
sincrono (ovvero fino a che tutte le azioni dipendenti non
hanno terminato la loro esecuzione allora nemmeno la fire
action termina la sua esecuzione). Nella prossima implementazione
del framework il legame fire action-listeners dipendenti sarà
gestito in modalità asincrona multithreaded.
Ecco come si configura una fire action:
<fireAction
NAME="is-delivery-needed" CLASS="com.mokabyte.example.BaseFireAction">
<listener>add-delivery-download</listener>
<listener>add-special-delivery</listener>
<executed PAGE="switch-delivery-needed.run"
FORWARD="true"/>
<failed PAGE="is-payment-online.run"
FORWARD="true"/>
</fireAction>
Per evitare di incorrere in problemi non facilmente gestibili
si consiglia al momento non associare ruoli di sicurezza alle
azioni listener. Anche le fire actions al momento non supportano
i ruoli.
Il
Controller
Come già accennato in precedenza il controller viene
configurato tramite due file XML, routings.xml e actions.xml:
il primo serve per definire le associazioni fra pagine JSP/HTML
e nomi simbolici mentre il secondo effettua lo stesso tipo
di associazione fra nomi ed azioni.
Senza entrare troppo nello specifico della analisi del codice
che implementa le varie funzionalità (cosa che verrà
fatta in un prossimo articolo) si può brevemente dire
che il controller è implementato da due servlet la
Actionservlet e la Routerservlet, le quali sono preposte alla
gestione degli URL invocati dal client.
Alla inizializzazione delle servlet sono caricati in memoria
i vari file di configurazione delle actions e dei routings.
Per quanto concerne gli aspetti legati alla configurazione
di una applicazione basata su MokaPackages non vi sono altri
particolari aspetti di cui tener conto circa il controller.
Si ricordi che le viste sono mappate su url *.show e le azioni
su URL pattern *.run. Tale mapping viene definito nel file
di deploy web.xml.
La View
La view (V di MVC) è costituita da una serie di pagine
JSP o anche semplicemente HTML.
Ogni volta che il client esegue la richiesta di visualizzare
una pagina di qualche tipo (JSP, HTML, altro) si dice che
si deve visualizzare una vista. In MVC una pagina HTML dinamica
non è altro che una visione del sistema sotto determinate
condizioni. Potrebbe essere assimilata al concetto di select
con condizione su una database relazionale, o ad una vista
selettiva di Lotus Notes.
Le viste nel framework sono associate a URL di tipo *.show:
tutte le volte che un utente esegue un link del tipo home.show
il controller intercetta tale chiamata e rimanda alla visualizzazione
della pagina indicata.
Ecco un semplice esempio di routings.xml
<routings>
<routing CODE="home.show" PAGE="index.jsp"
FORWARD="false"/>
<routing CODE="menu.show" PAGE="menu.jsp"
FORWARD="false">
<roles>
<role>user</role>
<role>admin</role>
<role>manager</role>
</roles>
</routing>
<routing CODE="test.show" PAGE="Test.jsp"
FORWARD="false"/>
<routing CODE="user-not-logged.show"
PAGE="userLogin.jsp" FORWARD="false"/>
<routing CODE="login-ko.show" PAGE="loginFailed.jsp"
FORWARD="false"/>
</routings>
Si
noti la possibilità di associare dei ruoli tramite
i quali definire regole di sicurezza associate ai routings.
Si parlerà più avanti di questo aspetto.
Se
la realizzazione delle varie viste è un compito a totale
carico del programmatore della web application, MokaPackages
partecipa alla realizzazione della view tramite una minimale
ma molto importante ed efficace tag library JSP.
Una analisi della MokaByte Custom Tag Library (MBTL), verrà
presentata in modo introduttivo più avanti ed in modo
dettagliato in un prossimo articolo di questa serie. La caratteristica
importante di MBTL è che oltre ad offrire alcuni custom
tag che semplificano la creazione delle pagine dinamiche,
include un potente quanto semplice meccanismo di gestione
dei template JSP. Tramite la definizione di pagine di template
e la possibilità di popolare le varie parti del template,
le web application possono essere sviluppate in modo realmente
molto semplice ed essere mantenute con pochissimo lavoro.
Nel paragrafo successivo dedicato alla MBTL verrà dato
un esempio di come ciò possa essere possibile.
Inizializzazione
Il framework offre un meccanismo di inizializzazione automatica
di una serie di beans al momento della creazione del contesto
o della sessione. Il tutto si basa sull'utilizzo di particolari
listener di contesto e di sessione in modo che le operazioni
di inizializzazione possano essere eseguite in modo automatico
indipendentemente dal punto di ingresso dell'utente nella
applicazione.
Questa logica permette di creare moduli atomici in cui inserire
le singole operazioni di inizializzazione.
Il sistema prevede due tipologie di bean di inizializzazione:
initbeans ed applicationbeans.
Beans di inizializzazione
Sono beans che implementano la classe InitBeanImpl (che deriva
dalla interfaccia InitBean).
Lo scopo di questo genere di bean è il seguente: la
procedura di inizializzazione di una web application è
spesso molto delicata ed uno dei maggiori desideri di ogni
programmatore è usare un qualche sistema che automaticamente
esegua l'inizializzazione della applicazione senza obbligare
il flusso dell'utente per determinate pagine della applicazione.
Ad esempio spesso accade che un utente acceda alla web application
senza seguire il normale flusso delle pagine (la seconda volta
che si accede alla web application, si salta la index.jsp
dato che la volta precedente l'utente aveva creato un bookmark
sulla paginaXXX.jsp).
Esiste una sottile differenza fra i due tipi di bean: i beans
di inizializzazione devono infatti quello di eseguire tutte
quelle operazioni di inizializzazione necessarie alla creazione
del contesto (quando parte l'applicazione web) o della sessione
(quando un nuovo utente accede alla applicazione).
Le operazione da eseguire possono essere inserite nel metodo
init() che viene invocato in callback dal sistema alla creazione
della sessione o del context.
I bean di inizializzazione devono essere pensati come componenti
che eseguono delle operazioni particolari solo di inizializzazione
(il metodo init() deve essere sempre ridefinito) e che potrebbero
anche non essere mai utilizzati all'interno della applicazione
(a differenza degli application beans).
Tramite il sistema di inizializzazione il programmatore potrà:
- definire
quali operazione debbano essere effettuate alla creazione
del contesto applicativo. Ogni init bean con scope context
verrà creato ed inizializzato alla creazione del
contesto, ovvero alla prima esecuzione della applicazione.
- definire
quali operazione debbano essere effettuate alla creazione
della sessione applicativa. Ogni initbean con scope session
verrà creato ed inizializzato alla creazione della
sessione utente. Ogni utente avrà il suo set di bean
di inizializzazione
Per
creare un initbean si deve creare un oggetto che implementi
l'interfaccia InitBean o più semplicemente estendere
la classe InitBeanImpl e ridefinire il metodo init() all'interno
del quale si dovranno inserire tutte le operazioni di inizializzazione.
Di seguito è riportato come esempio il metodo init
della Log4jInitBean init bean fornito di corredo con il framework
utile per eseguire l'inizializzazione del sistema Log4J.
public
void init(ServletContext servletContext) throws MokaByteInitException{
try{
Properties LogProperties = new Properties();
String configDirectoryPath =
servletContext.getInitParameter("configDirectoryPath");
if (configDirectoryPath == null ||
configDirectoryPath.equals("")){
logger.info("Il configDirectoryPath
è nullo si usa quello di default");
configDirectoryPath =
"WEB-INF";
}
String logPropertiesFilename = servletContext.getInitParameter("logPropertiesFilename");
if (logPropertiesFilename == null
|| logPropertiesFilename.equals("")){
logger.info("Il logPropertiesFilename
è nullo si usa quello di default");
logPropertiesFilename
= "log4j.properties";
}
logPropertiesFilename = "/"
+configDirectoryPath + "/" + logPropertiesFilename;
LogProperties.load(servletContext.getResourceAsStream(logPropertiesFilename));
org.apache.log4j.LogManager.resetConfiguration();
PropertyConfigurator.configure(LogProperties);
logger = Logger.getLogger(Log4jInitBean.class.getName());
logger.debug("----------------------------------------------------------");
logger.info(" Inizializzazione
Log4J effettuata ");
logger.debug("----------------------------------------------------------");
}
catch(Exception e){
String msg ="Exception while
Log4jInitBean.init():"+e;
logger.error(msg);
throw new MokaByteInitException(msg);
}
}
Ecco
invece la porzione del file init-beans.xml che definisce il
Log4jInitBean
<init-bean
name="log4jInitBean" classname="com.mokabyte.mvc.beans.commons.Log4jInitBean"
scope="context">
<init-params>
<init-param>
<param-name>logPropertiesFilename</param-name>
<param-value>log4j.properties</param-value>
</init-param>
</init-params>
</init-bean>
A
proposito di Log4J è bene tener presente che tutto
il sistema è predisposto per il log dei messaggi tramite
questo framework rilasciato dal gruppo Jakarta.
Per risolvere il problema dell'uovo e della gallina (se l'inzializzazione
del log avviene dentro un initbean, come faccio a loggare
tutti i messaggi di sistema prima della esecuzione del metodo
init di Log4jInitBean?) il sistema parte con una configurazione
di default la cui configurazione potrà essere trovata
dentro il metodo loadLog4J() del ContextListener (metodo che
fa parte del framework per cui fino a quando non rilasceremo
i sorgenti non è possibile per il lettore verificarne
l'implementazione), che viene poi sovrascritta non appena
si inizializza il Log4jInitBean.
Ogni init bean può ricevere una serie di proprietà
di inizializzazione tramite il tag XML <init-param>.
Non vi è nessuna regola da seguire a tal proposito:
se il bean non possiede una proprietà con quel nome
non verrà impostata, il valore passato perso senza
la generazione di alcuna eccezione.
Beans
applicativi
Accanto ai bean di inizializzazione vi sono gli application
beans: essi sono creati automaticamente e messi nello scope
corrispondente in modo che siano disponibili ad esempio in
una pagina JSP tramite il tag standard <jsp:usebean>.
Di fatto i bean applicativi non dovrebbero eseguire nessuna
operazione di inizializzazione. La definizione all'interno
di init-beans.xml permette di avere tali oggetti già
creati e disponibili ad applicazione avviata (a prescindere
da quale sia la prima pagina aperta da un utente).
Un
bean applicativo implementa l'interfaccia ApplicationBean
o estende la classe ApplicationBeanImpl.
Il significato di questo tipo di componenti è quello
di fornire una serie di oggetti che automaticamente siano
creati, inizializzati e posti in context/session alla partenza
della applicazione o alla creazione di una sessione. Ogni
bean di inizializzazione può includere nella sua definizione,
quella di uno o più bean applicativi tramite il tag
<use-bean>.
Lo scope di un bean applicativo è lo stesso del bean
di inizializzazione all'interno del quale esso viene definito.
Per ogni bean applicativo è possibile definire una
serie di proprietà di inizializzazione tramite apposito
tag ed opzionalmente un metodo da invocare alla creazione.
Nella definizione di un bean applicativo si può specificare
il valore di alcune sue proprietà e il nome di un metodo
che verrà eseguito alla creazione del application bean.
Se le proprietà sono molte o una proprietà è
ripetuta più volte per più bean si possono mettere
dentro il file application.properties in modo da generalizzare
il popolamento degli attributi.
I bean applicativi potranno poi essere utilizzati all'interno
delle pagine JSP tramite il tag <jsp:useben>
Ecco
un esempio del file init-beans.xml completo all'interno del
quale sono definiti sia init bean che bean applicativi.
<init-beans>
<init-bean name="log4jInitBean"
classname="com.mokabyte.mvc.beans.commons.Log4jInitBean"
scope="context">
<init-params>
<init-param>
<param-name>logPropertiesFilename</param-name>
<param-value>log4j.properties</param-value>
</init-param>
</init-params>
<use-bean name="contextApplicationBean"
classname="
com.mokabyte.testmvc.beans.ExampleApplicationBean">
<bean-properties>
<bean-property-name>simpleProperty</bean-property-name>
<bean-property-value>context</bean-property-value>
</bean-properties>
<method>
init
</method>
</use-bean>
</init-bean>
<init-bean
name="sessionInitBean"
classname="
com.mokabyte.testmvc.beans.ExampleInitBean"
scope="session">
<init-params>
<init-param>
<param-name>filename</param-name>
<param-value>log4j.properties</param-value>
</init-param>
</init-params>
<use-bean name="sessionApplicationBean"
classname="com.mokabyte.testmvc.beans.ExampleApplicationBean">
<bean-properties>
<bean-property-name>
simpleProperty
</bean-property-name>
<bean-property-value>
session
</bean-property-value>
</bean-properties>
<method>
init
</method>
</use-bean>
</init-bean>
</init-beans>
Dato
che per definire un bean applicativo è necessario includerlo
all'interno di un bean di inizializzazione, nel caso in cui
sia necessario creare un bean applicativo senza la necessità
di creare bean di inizializzazione, il sistema fornisce il
com.mokabyte.mvc.beans.commons.BlankInitBean un init bean
che non svolge nessuna operazione ma serve solo per crearne
uno applicativo.
Sicurezza,
autenticazione, ruoli
Una delle funzionalità più ricorrenti nello
sviluppo di una web application è quella relativa alla
gestione della autenticazione e sicurezza sulle azioni e sui
routing. Il framework prevede un meccanismo molto semplice
tramite il quale è possibile definire un insieme di
regole di protezione sugli accessi a pagine JSP (protezione
dei routings) che ad azioni specifiche (protezione delle actions).
Per comprendere facilmente come meccanismo di sicurezza funzioni
si deve analizzare la cosa da due punti di vista: protezione
di risorse tramite la definizione e gestione dei ruoli utente,
login ed accreditamento degli utenti.
Protezione
di risorse
Il meccanismo di protezione si basa sul concetto di ruolo
(role). Su ogni mapping (azioni o routings) possono essere
associati ad uno o più ruoli.
Solo se l'utente dopo l'operazione di login riceve un ruolo
fra quelli specificati per il mapping, allora l'utente potrà
eseguire il mapping: questo significa poter vedere una pagina
JSP o per le azioni poter eseguire l'azione.
Se nessun role è definito per un determinato mapping,
allora il mapping è pubblico e potrà essere
eseguito senza necessità di login.
I ruoli possono essere associati ai mapping in modo molto
semplice tramite i file di configurazione XML actions.xml
o routings.xml.
Per capire come si definisce un ruolo si partirà dalle
azioni prendendo in esame l'applicazione di esempio allegata
al progetto: in questo caso sono stati definiti alcuni mapping
su azioni ai quali sono stati associati dei ruoli: user, admin,
manager.
La scopo è definire tre azioni che potranno essere
eseguite solo dal ruolo manager (per la azione con mapping
manager-action.run), una solo dal ruolo manager ed admin (per
la azione con mapping admin-action.run) ed infine una utilizzabile
da tutti i tre i ruoli (per la azione con mapping user-action.run).
Tutte e tre le azioni non potranno essere eseguite da utenti
che non abbiano effettuato il login.
L'azione associata a questi mapping è di fatto sempre
la stessa, la classe RoledAction, dato che non è interessante
in questo caso implementare comportamenti differenti, ma solo
mostrare come associare ruoli diversi per azioni.
Il file actions.xml contiene quindi il seguente codice XML:
<action
NAME="manager-action.run" CLASS="com.mokabyte.testmvc.actions.RoledAction">
<executed PAGE="executed-ok.show"/>
<failed PAGE="executed-ko.show"/>
<roles>
<role>manager</role>
</roles>
</action>
<action
NAME="admin-action.run" CLASS="com.mokabyte.testmvc.actions.RoledAction">
<executed PAGE="executed-ok.show"/>
<failed PAGE="executed-ko.show"/>
<roles>
<role>admin</role>
<role>manager</role>
</roles>
</action>
<action
NAME="user-action.run" CLASS="com.mokabyte.testmvc.actions.RoledAction">
<executed PAGE="executed-ok.show"/>
<failed PAGE="executed-ko.show"/>
<roles>
<role>user</role>
<role>admin</role>
<role>manager</role>
</roles>
</action>
Con
questa organizzazione implicitamente si tende a fornire una
gerarchia ai ruoli: manager ha più poteri di admin
che a sua volta è più potente di user.
Analogamente si possono associare ruoli ai routings definiti
in routings.xml; ad esempio nella applicazione allegata, il
menu principale è accessibile solo dopo essersi loggiati
ed aver ricevuto un ruolo dal sistema. Per fare questo il
file routings.xml è così definito
<routing
CODE="menu.show" PAGE="menu.jsp" FORWARD="false">
<roles>
<role>user</role>
<role>admin</role>
<role>manager</role>
</roles>
</routing>
Al
momento il framework non dispone di nessun meccanismo per
la definizione dei ruoli abilitati nella applicazione (tutti
i ruoli che verranno definiti in actions.xml e routings.xml
sono ruoli buoni), così come manca un modo per specificare
la gerarchia sui ruoli. Nella prossima implementazione è
prevista la definizione di una gerarchia lineare (ruolo user
< del ruolo admin < ruolo manager) per cui se un mapping
(action o routing) è associato al ruolo admin esso
potrà essere eseguito sia da utenti con ruolo manager
che admin in quanto admin < manager ma non da utenti con
ruolo user.
La classe Role attualmente dispone di una proprietà
level, che potrà essere utilizzata per ridefinire il
controllo sui ruoli ed implementare così la gerarchia
fra ruoli.
Al momento non è presente nessun meccanismo per definire
in modo assoluto i ruoli: non esiste per così dire
un file roles.xml, con il quale definire quali ruoli sono
utilizzabili all'interno della applicazione e le relazioni
fra i ruoli stessi.
Manca adesso e non verrà implementato per una precisa
scelta filosofica la possibilità di aggregare i ruoli
in gruppi: nel mondo delle web application, non sono assimilabili
al concetto di utente/gruppo di unix.
Autenticazione
e controllo dei ruoli
Al momento del login l'utente riceve un ruolo (identificato
con un nome). Nel momento in cui un utente invoca un URL associato
ad una determinata azione o routing, il sistema ricava tutti
i ruoli associati all'azione e solo se il ruolo utente corrisponde
ad uno di questi ruoli l'azione potrà essere eseguita.
Come accennato nel paragrafo precedente il controllo attuale
verifica solo se il ruolo utente appartiene al set di ruoli
definiti per il mapping, non la gerarchia.
Ogni classe che effettui il login, dopo aver controllato in
vario modo che le credenziali d'autenticazione corrispondano
ad un utente accreditato nel sistema, può procedere
ad inserire in sessione un attributo di nome loggedUser di
classe LoggedUser, la quale contiene le informazioni minimali
per la rappresentazione di un utente loggato (userId, password,
ruolo, descrizione).
Nel caso si desideri realizzare una classe personalizzata
di login, sarà sufficiente avere attenzione a porre
in sessione un attributo come appena visto.
Ad esempio la SimpleLoginAction (che non effettua controlli
particolari ma simula l'autenticazione di un utente qualsiasi)
svolge queste operazioni all'interno del metodo di login
public
Routing perform(HttpServlet httpServlet,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse)
throws
ActionException {
String userId = httpServletRequest.getParameter("userId");
String userPassword = httpServletRequest.getParameter("userPassword");
logger.debug("login for userId="+userId+",
userPassword="+userPassword);
// accredita solamente gli utenti (admin,admin)
if
(userId.equals("simple") && userPassword.equals("simple")){
// crea un role di livello 0
Role role = new Role("default",
0);
// crea un ruolo loggedUser da mettere
in sessione
LoggedUser loggedUser = new LoggedUser(userId,
userPassword, role,
"Utente
creato dalla SimpleLoginAction");
HttpSession session = httpServletRequest.getSession();
//
pone in sessione l'oggetto loggedUser in modo che possa essere
autenticato // successivamente
session.setAttribute("loggedUser",loggedUser);
logger.debug("--- Exit perform()
with executed");
return this.getExecutedRouting();
}
else{
logger.debug("--- Exit perform()
with failed ");
return this.getFailedRouting();
}
}
Il
passaggio più importante di tutta questa procedura
è quello che pone in sessione l'oggetto loggedUser
al fine di rendere possibile in automatico il controllo sull'utente
nei passaggi successivi.
Questo requisito rappresenta un potenziale pericolo: il programmatore
che desideri realizzare una propria classe di login, potrebbe
per un qualche motivo omettere (per errore, per l'insorgere
di una eccezione o per un altro motivo) di salvare l'oggetto
loggedUser in sessione.
Per questo motivo il framework mette a disposizione un meccanismo
standard che separa la fase di autenticazione da quella di
gestione del profilo utente.
La action PluggedLoginAction funzione infatti come wrapper
di una classe (caricata dinamicamente tramite parametro XML)
che eseguirà materialmente l'operazione di autenticazione,
mentre il wrapper esegue le operazioni di contorno.
Il programmatore che desideri quindi implementare solo la
logica di autenticazione, tralasciando i dettagli legati alla
gestione del ruolo e del salvataggio del loggedUser in sessione,
dovrà semplicemente implementare un authenticator e
comunicare alla PluggedLoginAction di usarlo per la procedura
di controllo di username e passoword.
Per realizzare un authenticator si deve scrivere una classe
che implementi l'interfaccia
public
interface UserAuthenticator {
public LoggedUser login(String UserId, String
UserPasswd,
HttpServlet httpServlet, HttpServletRequest httpServletRequest);
}
procedendo
a ridefinire il metodo login.
Il framework offre implementazione di test, che consente di
effettuare delle prove rapide per procedere alla realizzazione
di prototipi semplici, la DefualtAuthenticator: essa consente
il login ad utenti con username e password [user,user] (in
questo caso role associato e' user), admin,admin (role assegnato
admin) e manager,manager (role assegnato manager).
Ecco l'implementazione del metodo di login:
public
LoggedUser login(String UserId, String UserPasswd,
HttpServlet
httpServlet,
HttpServletRequest
httpServletRequest) {
// questo metodo effettua una operazione di login
che accredita utente "user,user",
// oppure "admin,admin", oppure "manager,manager"
if (UserId.equals("user") &&
UserPasswd.equals("user")) {
Role SimpleRole = new Role("user",
0);
return new LoggedUser(UserId, UserPasswd,
SimpleRole,
"Un
ruolo utente molto semplice con role ="
+SimpleRole.toString());
}
else if (UserId.equals("admin") &&
UserPasswd.equals("admin")) {
Role SimpleRole = new Role("admin",
1);
return new LoggedUser(UserId, UserPasswd,
SimpleRole,
"Un
ruolo utente molto semplice con role ="
+SimpleRole.toString());
}
else if (UserId.equals("manager") &&
UserPasswd.equals("manager")) {
Role SimpleRole = new Role("manager",
2);
return new LoggedUser(UserId, UserPasswd,
SimpleRole,
"Un
ruolo utente molto semplice con role ="
+SimpleRole.toString());
}
else
return null;
}
Per
poter utilizzare la PluggedLoginAction con un autenticatore
caricato dinamicamente si deve scrivere nel file actions.xml
<action
NAME="login.run" CLASS="com.mokabyte.mvc.actions.commons.PluggedLoginAction">
<executed PAGE="login-ok.show"/>
<failed PAGE="login-ko.show"/>
<init-params>
<init-param>
<param-name>user-authenticator-classname</param-name>
<param-value>com.mokabyte.mvc.users.DefaultUserAuthenticator</param-value>
</init-param>
</init-params>
</action>
Si
noti la semplicità tramite la quale sia possibile sostituire
l'autenticatore di default con uno più realistico che
esegue il controllo su strato di persistenza.
Per questo motivo si consiglia di procedere alla personalizzazione
della procedura di login tramite la creazione di opportuni
autenticatori customizzati piuttosto che ridefinire da capo
tutta la procedura.
Prossimamente
E' attualmente in fase di implementazione una funzionalità
basata su custom tag che in base al ruolo assegnato al login,
mostri solo determinati voci di menu. Al momento questa funzionalità
è facilmente implementabile grazie al custom tag ConditionalInclude
fornito nella MBTL.
Gestione
dei messaggi d'errore
La gestione dei messaggi di errore è stata implementata
nel framework prendendo spunto da quella offerta da Struts.
Ogni volta che una azione voglia comunicare un determinato
evento, messaggio o semplicemente errore, potrà farlo
tramite un semplice meccanismo basato da un lato sulla eccezione
ActionException, dall'altro sulla classe Message per la visualizzazione.
L'applicazione allegata include un link all'URL fake.run associato
alla FakeAction (letteralmente azione fasulla), la quale non
svolge nessun compito, se non la generazione forzata di una
eccezione all'interno del metodo perform(). Ecco la sua implementazione
public
Routing perform(HttpServlet httpServlet,
HttpServletRequest httpServletRequest,
HttpServletResponse
httpServletResponse)
throws ActionException {
throw new ActionException("Un
errore generato appositamente per mostrare il funzionamento
della gestione degli errori in
MokaPackages");
}
Dopo
l'esecuzione della azione, il controller intercetta l'eccezione
che si è verificata e popola un apposito container
di messaggi di oggetti di classe Message.
Ecco un esempio
catch(ActionException
ae) {
// aggiunge un messaggio di errore da visualizzare.
il messageId potrà essere
// utilizzato in un secondo momento come id di
un boundle di messaggi
this.addMessage(httpSession, "actionException",ae.getMessage());
// si salta al routing creato appositamente per
saltare alla pagina di errore
errorRouting = new RoutingImpl("nullRouting",
"error",
this.getErrorPage(), false, false);
}
Al
di fuori della catch la ActionServlet esegue una altra serie
di operazioni fra cui rimandare alla pagina associata al routing
errorRouting (in genere errorPage.jsp).
Si noti che se si volesse generare una eccezione all'interno
della azione che non sia intercettata dal controller sarà
sufficiente non rilanciare dal metodo perform() una ActionException
ma una qualsiasi altra eccezione.
Ecco l'implementazione del metodo addMessage() della ActionServlet
private
void addMessage(HttpSession httpSession, String messageId,
String messageBody){
// ricava dalla sessione il properties con tutti
i messaggi.
// Se nullo ne crea uno nuovo
Properties messages = (Properties)httpSession.getAttribute("messages");
if (messages == null )
messages = new Properties();
// crea un oggetto di classe MessageBean da aggiungere
alla collection di messaggi
MessageBean messageBean = new MessageBean();
messageBean.setMessageBody(messageBody);
messageBean.setMessageId(messageId);
// aggiunge alla collection di messaggi il messaggio
appena passato come parametro.
// Come id di messaggio si usa messageId del messaggio
stesso
messages.put(messageId,messageBean);
// rimette in sessione il properties che contiene
tutti
httpSession.setAttribute("messages",
messages);
}
A
questo punto in sessione si trova un attributo con nome messages
che contiene tutti i messaggi che sono stati generati dal
sistema: tali messaggi potranno essere visualizzati nella
pagina di errore tramite il custom tag MessagesTag. Ecco come
si può semplicemente realizzare la cosa nella pagina
di errore
Attenzione
si sono verificati degli errori <BR><mbtl:messages/>
Il meccanismo al momento permette di utilizzare solamente
messaggi di errore statici (di fatto quello passato al costruttore
della ActionException), ma in una prossima implementazione
sarà possibile utilizzare messaggi localizzati (funzione
della lingua e/o del locale impostato) prelevati da un file
properties secondo il classico meccanismo dei boundle.
MBTL,
la Custom Tag library di MokaPackages
La MBTL è la tag library che fornisce alcuni semplici
ma efficaci tag custom JSP.
Per una analisi dettagliata dei vari tag servirebbe molto
spazio, più di quello qui a disposizione.
Nella applicazione di esempio allegata sono mostrati alcuni
tag. In un prossimo articolo verrà mostrata tutta la
MBTL nella sua interezza. Per il momento ci limiteremo ad
analizzare brevemente ed in modo introduttivo il meccanismo
di gestione dei template.
L'utilizzo dei template non è una prerogativa di MokaPackages,
dato che molti sistemi di presentazione ne fanno uso. In Struts
il package Tile fornisce questo tipo di funzione.
Utilizzare i template permette di concentrare in una o più
pagine JSP gli elementi grafici e non solo comuni a tutte
le pagine della applicazione. Ad esempio un buon grafico HTML
potrebbe generare una complessa struttura grafica inserendo
nel template tutti gli elementi di abbellimento della pagina
e definendo al contempo i settori della pagina che poi ogni
singola pagina JSP andrà a riempire, cambiando di volta
in volta i contenuti: le sezioni potrebbero essere "titolo
pagina", "barra dei menu", "corpo pagina",
"sezione banner", "header", "footer",
"menu laterali" e così via.
Per fare un esempio si potrebbe immaginare che tutte le pagine
includano un piccolo menu dove inserire le due voci autoesclusive
login/logout (a seconda che il login sia effettuato oppure
no), la voce di help, l'about.
A tal proposito si potrebbe pensare di includere questo menu
in una sezione del template in modo che in ogni pagina ci
si possa concentrare sulla modifica solo del contenuto e non
degli aspetti grafici del menu (che invece sarebbero inseriti
nel template).
Molto altro ci sarebbe da dire sull'uso dei template (ad esempio
si potrebbe immaginare una applicazione generica per la visualizzazione
di contenuti che cambiano in funzione del contesto: in tal
caso non solo le sezioni potrebbero essere dinamiche ma anche
il nome del template potrebbe essere caricato da database),
ma per il momento ci limiteremo a studiare come esso funzioni
in MokaPackages. L'esempio allegato dovrebbe più di
ogni altra spiegazione far comprendere come esso funzioni.
Per definire un template è sufficiente creare una pagina
JSP e definire al suo interno le sezioni che si desidera creare
tramite il tag <mbtl:define>. Ad esempio
<mbtl:define
section="topmenu" printable="true">
</mbtl:define>
definisce
una sezione dove dovranno essere inseriti i menu della pagina.
L'attributo section definisce il nome della sezione, mentre
printable specifica che all'interno della sezione potranno
essere inserito direttamente del codice HTML oppure una pagina
JSP (nel caso dei menù è preferibile).
A questo punto se si desidera comporre la pagina index.jsp
in modo che usi i template definito si deve usare il tag <mbtl:template>
e popolare al suo interno le varie sezioni con il contenuto
che si desidera. Ad esempio
<mbtl:template
name='pageTemplate' content='template.jsp'>
<mbtl:insert section='topmenu' element="topMenu.jsp"/>
<mbtl:insert section='pagetitle' printable='true'> MokaByte
MVC </mbtl:insert>
<mbtl:insert section='body' printable='true'> Funzionalità
della web application Test MVC </mbtl:insert>
.
</mbtl:template>
si
noti la differenza fra l'inserimento di un menu tramite file
JSP (topMenu.jsp) e la stampa diretta del testo come nella
sezione pagetitle.
Per il momento ci fermiamo qui. Analizzando l'applicazione
allegata si potranno scoprire molte altre cose, sperando che
le soluzioni adottate siano sufficientemente semplici per
essere scoperte senza l'ausilio di un manuale completo
.
Che poi è lo scopo primario di MokaPackages.
Setup
iniziale
Per poter utilizzare mokapackages è necessario effettuare
alcune modifiche al file di deploy web.xml. Si tratta di aggiunte
che consentono il mapping degli url pattern *.run e *.show
sui servlet del controller oltre ad alcuni parametri di configurazione
necessari per poter utilizzare la tag library MBTL.
Nel prossimo articolo mostreremo ogni singola parte del file.
Per il momento il lettore che vorrà utilizzare l'applicazione
di esempio o crearne una nuova potrà limitarsi a leggere
il web.xml inserito nella webapp di esempio.
Una
nota sul nome
Al tempo della sua nascita il framework non aveva nessuna
velleità di diventare un framework: era solo una collezione
di classi che offrivano delle funzionalità di base
per la realizzazione di applicazioni web secondo il modello
MVC.
Si iniziò con l'estrapolare da tutti i progetti da
noi portati avanti quelle classi comuni e quelle funzioni
più utili raccogliendole in un jar comune a tutte le
web application. Il framework non era ancora un framework,
ma si iniziò ad avere in tutti i progetti un file jar
che raccoglieva quei packages comuni a tutte le web application.
Il nome in codice che prese questo file jar fu inizialmente
mokapackages.jar e ci ripromettemmo di trovare un nome con
più sexappeal non appena avessimo un po' di tempo per
pensarci.
Come spesso accade però il nome non fu trovato e quindi
il set di classi comuni e' rimasto denominato mokapackages,
nome che almeno formalmente è ancora un nome in codice.
MokaPackages non contiene al suo interno solamente il framework
MVC, ma anche una tag library JSP minimale (che viene utilizzata
congiuntamente al framework MVC) ed uno strato di classi per
l'accesso a server Lotus Domino, che in questa prima fase
di rilascio non verrà presentata.
Nel framework sono poi presenti anche altre classi di pubblica
utilità.
E'
pur vero che al momento il framework pur essendo arrivato
alla versione 2.5, manca ancora di molte parti importanti
(validazione delle form, gestione automatica dei dati in input,
internazionalizzazione solo per citarne alcune), ma stiamo
lavorando per colmare tali lacune. Chiunque volesse unirsi
al progetto è benvenuto.
Bibliografia
[MVC] "MokaShop il negozio online di MokaByte: Progettare
applicazioni J2EE multicanale. I parte: il modello MVC, la
View ed il Controller" - di Giovanni Puliti, MokaByte
60 Febbraio 2002 - http://www.mokabyte.it/2002/02/devj2ee_1.htm
Risorse
Scarica l'applicazione di esempio
che mostra l'uso del framework
|