MokaByte 86 - Giugno 2004 
MokaPackages
Il framework MVC di MokaByte
di
Giovanni Puliti
Inizia con questo articolo la presentazione di un nuovo progetto della redazione di Moka-Byte, progetto che dovrebbe confluire nella realizzazione di un nuovo gruppo di lavoro open source.

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

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it