Feed RSS con JSF
Questo articolo si pone l’obiettivo di descrivere il progetto che abbiamo realizzato tra febbraio e aprile. Dovevamo scrivere un’applicazione web che consentisse la gestione e la divulgazione delle notizie degli uffici del Comune di Grosseto tramite l’uso dei feed RSS. Gli RSS sono dei files formattati in standard XML che contengono una lista (o feed) di titoli, sommari e links, recuperabili da speciali lettori, detti appunto RSS readers. Di lettori ce ne sono molti, sia Open Source sia commerciali. Di RSS non ne esiste un’unica versione, ma tante, con il risultato di trovarsi di fronte ad un dilemma per decidere quale sarà la versione o il formato universalmente valido. Noi abbiamo deciso di attenerci alla versione 2.0 (le cui specifiche possono essere lette all’indirizzo http://blogs.law.harvard.edu/tech/rss) perché è la versione più recente e più semplice da usare.
Per poter far uso dei feed RSS con le notizie delle varie direzioni comunali, è stato necessario creare una nuova applicazione per la diffusione delle notizie che ha rimpiazzato quella esistente. Partiamo col dire quali sono stati i nostri “ingredienti”. L’intero applicativo è stato scritto in Java utilizzando JavaServer Faces 1.1 (JSF) come framework e sviluppato con Oracle JDeveloper 10g 10.1.3,. Il nostro intento era anche quello di creare una procedura che fosse compatibile sia con prodotti commerciali sia con prodotti Open Source quindi abbiamo reso disponibile un’unica versione per gli application server: Oracle 10g (9.0.4.1 e 10.1.2), Oracle OC4J (10.1.2 e 10.1.3), Apache Tomcat 5.5 e per i database: Oracle 10g 9.0.4 e MySql 4.1.
Analizzando la struttura del nostro progetto, vediamo che è composto di due parti: una riguardante la parte di amministrazione, tramite la quale l’utente può effettuare inserimenti, modifiche e ricerche di notizie; l’altra ha il compito di recuperare dal database le informazioni offrendole come feed RSS attraverso l’uso di una servlet.
Cominciamo a descrivere la struttura del nostro database e di alcune tabelle.
Per prima cosa consideriamo la tabella “RSS_GOL” (Tabella 1) che ha un vincolo referenziale dal campo “TIPO_NOTIZIA” al campo ID della tabella “RSS_GOL_TIPONOTIZIE” (Tabella 2) di cui daremo dopo la descrizione. Solo tre campi in questa tabella devono essere obbligatoriamente popolati: il campo TITLE che contiene il titolo della notizia; il campo DESCRIPTION che contiene il testo della notizia e ovviamente ID che è una chiave primaria. Poniamo l’accento sul campo PUBDATE che ci permette di tenere traccia della data e dell’ora dell’inserimento della notizia. Abbiamo scelto un campo di tipo Timestamp perché permette di memorizzare un dato formato sia dalla data sia dall’ora, altrimenti avremmo dovuto spezzare in due tale campo, ricorrendo ad uno di tipo Date e ad un altro di tipo Time e perdendo parzialmente la compatibilità tra i due database usati. Ci è sembrato opportuno utilizzare una soluzione unificata mediante un campo di tipo più complesso.
Campo | Tipo | NULL | Chiave | Default |
TITLE | varchar(150) |
|
|
|
LINK | varchar(100) | SI |
| NULL |
DESCRIPTION | text |
|
|
|
CREATOR | varchar(8) | SI |
| NULL |
CATEGORY | varchar(40) | SI |
| NULL |
COMMENTS | varchar(250) | SI |
| NULL |
GUID | varchar(100) | SI |
| NULL |
PUBDATE | timestamp | SI |
| CURRENT_TIMESTAMP |
SOURCE | varchar(100) | SI |
| NULL |
DIREZIONE | int(5) | SI |
| NULL |
TIPO_NOTIZIA | int(3) | SI | MUL | NULL |
ENCLOSURE_URL | varchar(100) | SI |
| NULL |
ENCLOSURE_LENGTH | int(7) | SI |
| NULL |
ENCLOSURE_TYPE | varchar(20) | SI |
| NULL |
ID | int(7) |
| PRI | 0 |
Tabella 1
La Tabella 2, “RSS_TIPO_NOTIZIE”, contiene i tipi di notizie che possiamo inserire, ad esempio “Informazioni sito” oppure “Avvenimenti”. Non è ammesso inserire campi nulli all’interno della tabella.
Campo | Tipo | NULL | Chiave | Default |
ID | int(3) |
| PRI | 0 |
TIPO | varchar(30) |
|
|
|
Tabella 2
C’è poi una tabella “RSS_GOL_CHANNEL” (Tabella 3) che contiene i dati relativi alle caratteristiche del feed, quali il nome canale della notizia, l’URL della pagina che corrisponde al canale, la descrizione, la data di pubblicazione e l’ultima data di modifica delle notizie inserite. Alcuni campi non sono stati utilizzati: TEXTINPUT specifica una richiesta di input che può essere visualizzata con il canale; CLOUD specifica un web service che supporta l’interfaccia rssCloud che può essere implementata in HTTP-POST, XML-RPC oppure SOAP 1.1.
La specifica degli RSS 2.0 impone che siano presenti le voci TITLE, LINK e DESCRIPTION, dati che descrivono il canale cui appartengono le notizie che andremo a recuperare tramite i lettori di feed. Una parola va spesa su cosa si intende per “titolo del canale”: esso è il nome con cui gli utenti si riferiscono al nostro servizio di feed. Se avessimo un sito HTML che contenesse le stesse informazioni riportate nel feed RSS, allora il titolo del canale dovrebbe essere lo stesso del nostro sito.
Da notare anche due campi particolari, SKIPDAYS0 e SKIPHOURS0, i quali sono stati riportati all’interno della nostra tabella ed è stato assegnato loro il valore nullo. Questi campi servono a dire agli aggregatori quando possono evitare di leggere il nostro feed. Dobbiamo però aggiungere che ci sono ben 7 campi per gli “SKIPDAYS” (da SKIPDAYS0 a SKIPDAYS6) e ben 24 per gli “SKIPHOURS” (da SKIPHOURS0 a SKIPHOURS23). A titolo d’esempio, ne abbiamo riportati solo due per farne capire la struttura. Cosa contengono esattamente questi due set di campi? Il primo set (SKIPDAYS) accetta come valori i numeri da 0 a 6, che rappresentano i giorni da Lunedì alla Domenica. Quando la servlet genera il codice xml del feed ed uno di questi campi è presente, viene stampato il giorno inglese corrispondente al numero immesso (0 a Monday, 1 a Tuesday, etc. etc.). Il secondo set (SKIPHOURS) accetta invece i valori da 0 a 23 e ciascuno indica la corrispondente ora del giorno nell’arco delle 24 ore. Si ricorda che anche questi due campi sono totalmente opzionali e la loro omissione non inficia la specifica degli RSS 2.0.
Il campo PUB_DATE, come nel caso della tabella “RSS_GOL”, è composto da una parte che descrive la data e da un’altra che descrive l’orario. Anche questo campo è di tipo Timestamp per renderlo compatibile sia con Oracle sia con MySQL. E’ doveroso ricordare che questi database intendono in modo diverso i campi “data/ora”. Per Oracle ogni campo Date è un campo “data/ora”, mentre per MySQL Date viene utilizzato unicamente per la data e Time per l’orario. Dovendo quindi fare una scelta che andasse bene per entrambi i sistemi, abbiamo scelto di utilizzare il tipo Timestamp, altrimenti avremmo dovuto fare sia per “RSS_GOL” sia per “RSS_GOL_CHANNEL” un campo per la data e uno per l’ora. Va poi rilevato un ulteriore limite di MySQL, ed è quello di poter mettere solo un campo “data/ora” per tabella. Diverso è invece il discorso per Oracle in cui “data/ora” è il tipo di default.
Campo | Tipo | NULL | Chiave | Default |
TITLE | varchar(100) |
|
|
|
LINK | varchar(100) |
|
|
|
DESCRIPTION | text |
|
|
|
LANGUAGE | varchar(30) | SI |
| NULL |
COPYRIGHT | varchar(30) | Si |
| NULL |
MANAGING_EDITOR | varchar(50) | SI |
| NULL |
WEBMASTER | varchar(50) | SI |
| NULL |
PUB_DATE | timestamp | SI |
| CURRENT_TIMESTAMP |
CATEGORY | varchar(30) | SI |
| NULL |
GENERATOR | varchar(50) | SI |
| NULL |
DOCS | varchar(100) | SI |
| NULL |
CLOUD | varchar(100) | SI |
| NULL |
TTL | varchar(5) | SI |
| NULL |
IMAGE_TITLE | varchar(50) | SI |
| NULL |
IMAGE_URL | varchar(100) | SI |
| NULL |
IMAGE_LINK | varchar(100) | SI |
| NULL |
RATING | varchar(20) | SI |
| NULL |
TEXTINPUT | varchar(50) | SI |
| NULL |
SKIPHOURS0 | char(2) | SI |
| NULL |
SKIPDAYS0 | char(1) | SI |
| NULL |
ID | int(5) |
|
| 0 |
Tabella 3
Queste 3 tabelle caratterizzano il nostro progetto ed anche se ce ne sono altre (di cui parleremo in seguito), queste sono sicuramente quelle più importanti e su cui era necessario spendere qualche parola di chiarimento.
Analizzando ulteriormente il progetto è da notare che, nella parte di MODEL, per accedere al database facciamo uso unicamente della tecnologia standard JDBC, che si occupa di tre cose fondamentali:
- Stabilire una connessione al database
- Inviare comandi SQL
- Processare i risultati ottenuti
Nel nostro caso, il vantaggio di usare tale libreria è evidente perché ci permette di scrivere un’unica applicazione non curandoci del tipo di database a cui andremo a collegarci. Se avessimo fatto uso di altri metodi, sicuramente ci saremmo dovuti attenere all’architettura proprietaria di ciascun database a cui avremmo fatto accesso, ed eventualmente, il progetto non si sarebbe potuto considerare open source perché molti metodi di model o di business logic sono proprietari (come ad esempio Oracle ADF Business component).
Per ottenere una connessione al database abbiamo utilizzato un data source. Di cui parleremo in seguito. Prima di eseguire un’applicazione di tipo enterprise dobbiamo effettuare il deployment (o distribuzione) della stessa sull’application server. Per fare ciò dobbiamo impacchettare l’applicazione in un file WAR oppure EAR. Questi file contengono l’applicazione pronta per essere distribuita ed eseguita. Cerchiamo di capire più in dettaglio la composizione di tali files. Per prima cosa bisogna dire che, a seconda dell’application server, dovremo scegliere l’uno o l’altro formato. Se intendiamo utilizzare Oracle OC4J dovremo prendere in considerazione il file EAR, mentre se vogliamo utilizzare Tomcat dobbiamo necessariamente scegliere il formato WAR. Una differenza che possiamo notare è che il file EAR viene utilizzato per application server J2EE-compliant (ad esempio Oracle OC4J), ciò significa che ci sono delle caratteristiche ben precise e uno standard a cui devono attenersi tali applicazioni. Ad esempio, nei files EAR la context root può essere definita dall’utente modificandola all’interno del file descrittore del deployment, mentre i file WAR (vedi Tomcat) non sono J2EE-compliant e come context root prendono il nome del file WAR stesso. Possiamo quindi avere più istanze dello stesso progetto semplicemente cambiando il nome del file WAR e facendone il deployment, cosa non possibile per l’altro tipo di file, a meno di non cambiare, di volta in volta, la voce relativa alla context root. Parlando di deployment bisogna ricordare che non sempre è necessario inserire in tali files tutte le librerie necessarie al corretto funzionamento delle applicazioni, infatti nel nostro elaborato in certi casi abbiamo deciso di inserire librerie comuni a più applicazioni in directory accessibili direttamente dall’application server. Nel nostro caso abbiamo utilizzato le seguenti librerie:
- JSP Runtime
- Commons Beanutils 1.6.1
- Commons Collections 2.1
- Commons Digester 1.5
- Commons Logging 1.0.3
- JSTL 1.1
- JSF
- Oracle JDBC
- MySQL Connector/J
Alcune di queste non vengono inserite correttamente dagli application server nelle rispettive directory che contengono le librerie pur essendo presenti all’interno della directory /WEB-INF/lib del file WAR (o EAR). Abbiamo dunque copiato a mano queste librerie sia nella directory di librerie condivise del Tomcat (in /usr/local/tomcat/jakarta-tomcat-5.5.4/common/lib) che in quella dell’OC4J (in /usr/local/tomcat/oc4j1013/j2ee/home/lib).
Abbiamo accennato che le librerie del progetto sono contenute all’interno del file WAR/EAR, ma cosa altro contiene questo tipo di file? Sia l’uno sia l’altro sono file che possono essere compressi mediante il metodo di compressione ZIP e sono praticamente uguali fatta eccezione per la presenza, all’interno del file EAR, del file WAR e di una directory chiamata meta-inf contenente dei files XML di configurazione. Ci soffermeremo ad analizzare il file WAR, visto che non ci sono altre differenze con il file EAR (almeno dal punto di vista di forma). Proviamo dunque a decomprimere il nostro file WAR, otterremo una struttura come quella riportata, in cui sono presenti diverse directory:
/
/css
/imgs
/it
/js
/mypackage
/protectRSS
/WEB-INF
Abbiamo diviso in questo modo il progetto: nella directory principale (quindi in “/”) troviamo il file riguardante il menù principale, la pagina della gestione degli errori, altri files con le informazioni relative alla creazione del WAR (o EAR), il file con i messaggi dell’applicazione e, cosa importante, troviamo il file config.properties di cui parleremo più avanti. Nella directory /css troviamo il foglio di stile per le pagine JSF, in /imgs troviamo lo sfondo e le immagini, in /it sono contenute tutte le classi e i backing bean del nostro progetto, in /js troviamo il JavaScript che serve per la visualizzazione e gestione del pop-up del calendario per l’inserimento delle date nelle varie form. La directory /mypackage contiene altre classi e backing bean, /protectRSS è la directory che contiene le pagine JSF, le varie form d’inserimento e di ricerca ed è anche la directory che andremo successivamente a proteggere con l’autenticazione mentre /WEB-INF contiene il file web.xml che è un importante file di configurazione, in cui abbiamo definito diversi settaggi e di cui parleremo più avanti in dettaglio. Abbiamo accennato alla presenza di un file chiamato config.properties: in questo file, che è file di testo, andremo a definire dei parametri, primo tra tutti il nome JNDI (Java Naming and Directory Interface). I nomi JNDI hanno un ruolo vitale nelle intranet e in Internet, in quanto permettono la condivisione di una varietà di informazioni circa gli utenti, le macchine, le reti, i servizi e le applicazioni; permettono in pratica di associare nomi ad oggetti e di recuperare oggetti in base al loro nome. Quando si lavora con applicazioni distribuite, i vari componenti non possono localizzarne altri, c’è dunque bisogno di un’altra entità che permetta ai vari componenti di localizzarsi, ed è ciò che fa il JNDI. Esso lavora su due livelli ben definiti: uno client ed uno server. Il primo si occupa dell’interfaccia tra le applicazioni e il servizio, il secondo invece si occupa della manutenzione e risoluzione delle associazioni nomi-oggetti, del controllo dell’accesso e della gestione delle operazioni eseguite sulla struttura del servizio di directory.
Tornando al file di configurazione, se lo aprissimo con un qualsiasi editor di testi, vedremmo una struttura simile a questa, in cui definiamo il nome JNDI sia per Oracle sia per MySQL, più altre voci che per il momento non andremo ad analizzare.
# dati riguardanti il datasource
# togliere il commento all'uno o all'altro JndiName
# per Oracle
JndiName = jdbc/DB10gDS
# per MySQL
#JndiName = jdbc/MySqlDB
# ci dice se stiamo usando un DB Oracle
# se true sto usando Oracle, se false uso MySQL
isOracle = true
#isOracle = false
# nome e locazione file PHP
filePhp = /usr/local/tomcat/jakarta-tomcat-5.5.4/webapps/rss/newsfile.php
# numero massimo di record da leggere per il file PHP
numero_max_record_php = 5
# numero massimo di feed da generare
numero_max_feed = 30
Da solo il nome JNDI non permette di connettersi al database. E’ necessario intervenire modificando, all’interno dell’application server che andremo ad usare, il suo file (o più di uno) di configurazione relativo ai datasources. Prendiamo in considerazione il Tomcat. Abbiamo deciso di scrivere le informazioni per accedere ai vari database nel file context.xml presente nella directory “<INSTALL_DIR>/conf” (dove <INSTALL_DIR> è la directory dove è stato installato l’application server: nel nostro caso è in “/usr/local/tomcat/jakarta-tomcat-5.5.4”) e riportando gli stessi nomi JNDI che abbiamo definito poco sopra nel file di configurazione.
<!-- The contents of this file will be loaded for each web application -->
<Context>
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>META-INF/context.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
<Resource name="jdbc/DB10gDS" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000" username="rss" password="9876" driverClassName="oracle.jdbc.driver.OracleDriver" url="jdbc:oracle:thin:@db01.comune.grosseto.it:1521:db1"/>
<Resource name="jdbc/MySqlDB" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000" username="rss" password="9876" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/rss?autoReconnect=true"/>
</Context>
Potevamo anche scegliere di mettere queste informazioni nel file server.xml o in minimal-server.xml ma ci è sembrata una soluzione più funzionale mettere tali informazioni nel file context.xml perché così le informazioni sono disponibili a tutte le applicazioni del contenitore e non solo ad un particolare contesto. Se prendiamo invece in considerazione Oracle OC4J, dobbiamo andare ad intervenire sul file data-sources.xml che si trova in “<INSTALL_DIR>/j2ee/home/config“ (per noi è in “/usr/local/tomcat/oc4j1013/j2ee/home/config”). Anche qui andremo a definire il nome JNDI e i parametri di connessione per ogni database a cui intendiamo collegarci.
<connection-pool name="connection-pool-RSSsuDB1">
<connection-factory factory-
user="rss"
password="9876"
url="jdbc:oracle:thin:@db01.comune.grosseto.it:1521:db1">
</connection-factory>
</connection-pool>
<managed-data-source name="connection-RSSsuDB1"
connection-pool-name="connection-pool-RSSsuDB1"
jndi-name="jdbc/DB10gDS"/>
<connection-pool name="connection-pool-MySQL">
<connection-factory factory-
user="rss"
password="9876"
url="jdbc:mysql://localhost:3306/rss">
</connection-factory>
</connection-pool>
<managed-data-source name="connection-MySQL"
connection-pool-name="connection-pool-MySQL"
jndi-name="jdbc/MySqlDB"/>
Accennando al file di configurazione config.properties, abbiamo solo affermato che è un file di testo che contiene parametri di configurazione. Non abbiamo però detto come vi accediamo dall’interno della nostra applicazione Java. Abbiamo definito un metodo all’interno della nostra classe it.grosseto.comune.rss.DB che si occupa di leggerlo. E’ da notare che questo file di configurazione è un ResourceBunble e la variabile prop non è altro che un oggetto della classe java.util.ResourceBundle. Ma che caratteristiche ha questo file? In generale un ResourceBundle serve a scrivere un programma che può essere facilmente tradotto in più lingue o modificato successivamente per personalizzare certi aspetti del programma come messaggi o, come nel nostro caso, parametri per il buon funzionamento dell’applicazione. Va osservato che ogni chiave del ResourceBundle è di tipo String.
public void leggiConfig() throws Exception
{
ResourceBundle prop = ResourceBundle.getBundle("config");
JndiName=prop.getString("JndiName");
filePhp=prop.getString("filePhp");
String temp="";
temp=prop.getString("numero_max_record_php");
numero_max_record_php=Integer.parseInt(temp);
temp=prop.getString("isOracle");
if (temp.equalsIgnoreCase("false")) { isOracle=false; }
else if(temp.equalsIgnoreCase("true")) { isOracle=true; }
}
Il ResourceBundle non era l’unica possibilità per gestire i parametri di configurazione, potevamo ad esempio metterli all’interno del file web.xml e recuperarli dal contesto, come in questo caso di prova; i parametri sarebbero stati visti da tutte le classi e servlet.
<context-param>
<description>questo parametro appartiene al contesto, lo vedono tutte le servlet</description>
<param-name>param1</param-name>
<param-value>prova</param-value>
</context-param>
Per leggere tale dato basta fare:
String param1 = getServletConfig().getServletContext().getInitParameter("param1");
Invece per limitare la visibilità di un parametro ad una data servlet o ad una classe, bastava inserire queste impostazioni all’interno del file web.xml:
<servlet>
<servlet-name>ParametriIniziali</servlet-name>
<servlet-class>mypackage1.ParametriIniziali</servlet-class>
<init-param>
<description>Questo parametro appartiene solo a questa servlet</description>
<param-name>paramServlet</param-name>
<param-value>soloPerQuestaServlet</param-value>
</init-param>
</servlet>
Per recuperare tale dato bisogna fare:
String paramServlet = getServletConfig().getInitParameter("paramServlet");
Tornando al parametro JNDI, una volta che è stato recuperato tramite uno di questi modi, basterà semplicemente usare un altro metodo, presente nella classe it.grosseto.comune.rss.DB per poter effettuare la connessione al database. Context è un oggetto appartenente alla classe javax.naming.Context mentre InitialContext fa parte di javax.naming.InitialContext. InitialContext è il contesto di partenza per eseguire le operazioni di associazione dei nomi, le quali sono relative ad un contesto che implementa l’interfaccia Context, fornendo la base per la risoluzione dei nomi.
public Connection connectDB() throws Exception
{
Context inic = new InitialContext();
Context ctx = (Context) inic.lookup("java:comp/env");
DataSource nativeDS = (DataSource) ctx.lookup(JndiName);
Connection conn = nativeDS.getConnection();
return conn;
} // connectDB
Come abbiamo detto, nella nostra applicazione è presente un altro importante file di configurazione, il file web.xml nel quale andremo ad inserire delle altre informazioni relative ai datasource come riportate in esempio:
<resource-ref>
<description>Connessione a MySql</description>
<res-ref-name>jdbc/mysqlDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
<resource-ref>
<description>Connessione a Oracle</description>
<res-ref-name>jdbc/DB10gDS</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
Ci vorremmo soffermare ora su alcuni pezzi significativi di codice tratti dal nostro progetto e mostrare come abbiamo operato. Consideriamo dunque la situazione in cui, dalla form di ricerca, vogliamo trovare dei record che abbiano i requisiti che ci interessano. Il metodo che andiamo a chiamare accetta in ingresso l’identificativo della notizia, la sua data di inizio e di fine pubblicazione, la direzione dell’ufficio comunale, il tipo di notizia ed anche l’username della persona che sta eseguendo la ricerca. Questo metodo si chiama searchByKeys, fa parte di it.grosseto.comune.rss.DB, e ha compito di popolare la variabile risultati che è un Vector appartenente alla classe java.util.Vector. Il Vector è come un array (vi possiamo quindi accedere tramite un indice) ma la differenza è che può variare la sua dimensione. Una volta che la variabile dei risultati è stata popolata, viene mostrata la pagina di riepilogo di ciò che è stato trovato. Qui viene riportato il codice che si occupa di popolare la variabile e che eventualmente si occupa di mostrare, nella pagina degli errori, un messaggio che informa l’utente sul motivo per cui la ricerca non è andata a buon fine.
public String searchbyKeys() throws Exception
{
tipo_notizia="1"; // Vogliamo cercare solo le notizie contrassegnate come "Informazioni sito"
try
{
risultati = db.searchByKeys(id,dataStart,dataEnd,direzione,tipo_notizia,utenteLoggato);
max=risultati.size();
return "success";
}
catch (Exception e)
{
FacesContext context = FacesContext.getCurrentInstance();
FacesMessage message = new FacesMessage("Eccezione verificatasi durante la ricerca nel database. " +e.toString());
context.addMessage("errorForm", message);
return "failure";
}
}
Una volta che il metodo seachByKeys restituisce il Vector riempito con qualche dato, c’è da recuperare le informazioni delle singole componenti. Esse sono formate da dati complessi in quanto ogni componente è un bean che contiene le seguenti informazioni: l’ID, la data di pubblicazione, la direzione comunale e il tipo di notizia. Ci troviamo dunque davanti alla situazione di dover recuperare dati da un Vector di bean e lo vogliamo fare utilizzando i metodi messi a disposizione da JSF. Nel frammento di codice poco sotto riportato, facente parte della pagina dei risultati, abbiamo evidenziato tre blocchi interessanti in cui abbiamo cercato di risolvere altrettante nostre esigenze. La prima era quella di creare una tabella che avesse una riga alternativamente bianca o grigia chiara, per far questo abbiamo utilizzato il tag <c:set>. Tramite questo tag, ad ogni iterazione di lettura dell’elemento del Vector, andavamo ad assegnare il valore su cui, tramite <c:choose>, <c:when> e <c:otherwise>, abbiamo poi effettuato la scelta sul colore da utilizzare per la riga corrente.
La seconda era quella, appunto, di accedere ai valori della nostra variabile contenente i risultati e abbiamo deciso di farlo tramite il tag <c:forEach>. In questo modo abbiamo potuto iterare la lettura dei risultati della ricerca e stamparli a video all’interno della tabella.
L’ultima esigenza era quella di recuperare il valore risultato.id che veniva letto ad ogni iterazione, facendo in modo di renderlo sempre disponibile. Tale valore sarebbe stato passato in seguito ad un altro metodo che si sarebbe occupato, tramite un bottone presente su ogni riga della tabella, di recuperare dal nostro database il record associato a tale valore che funge da identificativo. Una volta letto il record, sarebbe poi stata mostrata la form in cui avremmo potuto modificare la notizia. Per fare questo abbiamo utilizzato un tag HTML di input che passa il risultato.id ad una variabile “hiddenId” in modalità appunto hidden, totalmente trasparente nonché nascosto agli occhi dell’utente.
<table border="2">
<tr>
<td >
<h:outputText value="#{Message.id}"/>
</td>
<td >
<h:outputText value="#{Message.pubdate}"/>
</td>
<td >
<h:outputText value="#{Message.direzione}"/>
</td>
</tr>
<c:set var="indice" value="0"/>
<c:forEach items="${Search.risultati}" var="risultato">
<h:form id="resultForm">
<h:message for="resultForm"/>
<c:set var="indice" value="${indice+1}"/>
<c:choose>
<c:when test="${indice % 2 == 0}">
<tr >
</c:when>
<c:otherwise>
<tr>
</c:otherwise>
</c:choose>
<td>
<c:out value="${risultato.id}" />
</td>
<td>
<c:out value="${risultato.pubDate}" />
</td>
<td>
<c:out value="${risultato.direzione}" />
</td>
<td>
<input type="hidden" name="hiddenId" value="${risultato.id}"/ >
<h:commandButton id="view" action="#{Search.view}" value="#{Message.view_button}" />
</td>
</tr>
</h:form>
</c:forEach>
</table>
Certamente ci potevano essere più modi per risolvere questi tre aspetti, magari l’uso di qualche altro framework tipo Struts.
Ricordando il contenuto del file config.properties, c’è da sottolineare che abbiamo inserito una speciale chiave che ci permette di sfruttare certe differenze tra Oracle e MySQL per le query sulle date. Stiamo parlando della chiave isOracle la quale, trattandosi di una variabile booleana, può avere solo due valori (true o false) e a seconda di questo utilizzeremo nel primo caso la sintassi di Oracle, nel secondo quella di MySQL. Come si vede guardando i frammenti di codice successivi, questi due database hanno un operatoreData simile, mentre hanno un formatoData molto diverso, pur mantenendo uguale la sintassi per il confronto sul campo di tipo data. Le variabili operatoreData, formatoData e where sono degli oggetti String, isOracle è di tipo boolean mentre dataStartSql è un oggetto java.sql.Date.
if (isOracle==true) {
operatoreData="TO_DATE";
formatoData="'yyyy-MM-dd'";}
else {
operatoreData="STR_TO_DATE";
formatoData="'%Y-%m-%d'";
}
Per operare invece un confronto sulle date, entrambi i database accettano ad esempio questa sintassi:
where=where.concat("pubdate >= "+operatoreData+"('"+dataStartSql+"', "+formatoData+")");
Ponendo nuovamente l’attenzione alle date, è interessante vedere come abbiamo utilizzato i campi di tipo Timestamp. I metodi che abbiamo utilizzato sono getTimestamp e setTimestamp, appartenenti il primo a java.sql.ResultSet e il secondo a java.sql.PreparedStatement. La classe java.sql.ResultSet contiene i metodi di lettura (getBoolean, getLong, getString e molti altri) dei valori che sono stati ottenuti eseguendo una qualche interrogazione al database che realizziamo attraverso il metodo prepareStatement della classe java.sql.PreparedStatement. Qui di seguito abbiamo riportato un altro frammento di codice in cui facciamo vedere l’uso del metodo prepareStatement e del setTimestamp; quest’ultimo prende in ingresso il numero della colonna presente nella riga di statement e il parametro che rappresenta il Timestamp. Per generare il timestamp facciamo ricorso alla creazione di un nuovo oggetto appartenente alla classe java.sql.Timestamp che a sua volta prende in ingresso un oggetto di tipo long che caratterizza il numero di millisecondi a partire dalla data di riferimento “January 1, 1970, 00:00:00 GMT”. La data attuale, che è un oggetto della classe java.util.Date, viene trasformata in millisecondi dal suo metodo getTime. Il metodo prepareStatement invece è applicato alla variabile conn che è un oggetto di tipo Connection appartenente alla classe java.sql.Connection che si occupa di inviare le stringhe SQL al database. Nel nostro esempio, effettuiamo l’inserimento della data/ora corrente nel campo pubdate che fa parte della tabella “RSS_GOL” e terminiamo la preparazione dei dati da inviare con l’esecuzione del metodo execute.
PreparedStatement inserimento=conn.prepareStatement("insert into rss_gol (pubdate) values (?)");
java.util.Date dataNow = new java.util.Date();
inserimento.setTimestamp(1,new java.sql.Timestamp(dataNow.getTime()));
inserimento.execute();
Volendo invece formattare un campo data/ora, letto dal nostro database, facciamo uso del metodo getTimestamp di cui abbiamo parlato poco sopra. Tale dato lo andremo poi ad inserire, come da nostro esempio, in pubdate, parametro appartenente ad un backing bean chiamato Insert, diverso da quello da cui è stato estrapolato questo pezzo di codice. Per permettere l’utilizzo di variabili di bean diversi, o meglio, per poter accedere a tutte le informazioni relative ad una singola richiesta JavaServer Faces, ricorriamo al FacesContext appartenente a javax.faces.context.FacesContext. Per formattare la data o l’ora con un certo pattern, facciamo uso di alcuni metodi della classe java.text.SimpleDateFormat ed in particolare dei metodi format e parse. Il primo serve a formattare e a convertire un oggetto data/ora che leggiamo con il metodo getTimestamp in una stringa; il secondo invece permette di convertire una stringa data/ora in una variabile di tipo java.util.Date. Per inserire questa data all’interno della variabile pubdate, appartenente al backing bean Insert, utilizziamo il suo metodo di set. Nel nostro esempio, la data viene formattata per mostrare giorno, mese, anno, ore, minuti e secondi.
FacesContext context = FacesContext.getCurrentInstance();
Insert insert= (Insert)context.getApplication().getVariableResolver().resolveVariable(context,"Insert");
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
String data_completa;
try
{
data_completa=sdf.format(rs.getTimestamp("pubdate"))+" " sdf.format(rs.getTimestamp("pubdate"));
}
catch (Exception ex) { data_completa=""; }
try {insert.setPubdate(sdf.parse(data_completa)); }
catch (Exception ex) { insert.setPubdate(null);}
Terminando con l’argomento riguardante le date, ribadiamo che avremmo potuto usare anche i metodi getDate/setDate e getTime/setTime ma avremmo dovuto creare due campi, uno per la data ed uno per l’orario, invece di uno più complesso che racchiudesse entrambi i tipi.
Soffermiamoci ora sul pezzo significativo di codice che ci ha permesso di generare il numero progressivo da scrivere nel campo ID della tabella di “RSS_GOL”, poiché tale campo rappresenta la chiave primaria. Avevamo bisogno di un modo per generare automaticamente tale valore che doveva essere univoco. A tal riguardo abbiamo deciso di usare una sequence ma questa soluzione se in Oracle è direttamente implementata, in MySQL non lo è ed è stato necessario creare qualcosa che ci si avvicinasse. Cosa è dunque esattamente una sequence? E’ una tabella in cui è memorizzato il valore successivo, il precedente e l’ultimo utilizzato. Oracle mette a disposizione questo genere di struttura. La sintassi per la generazione del valore successivo da dare all’identificativo della notizia è facile, come si vede nell’esempio. Nel caso di MySQL, invece, abbiamo creato una tabella chiamata “RSS_SEQ_MySQL” (Tabella 4) costituita da un unico campo non nullo. Per evitare che un ID fosse associato a più notizie differenti, abbiamo provveduto a bloccare tale tabella in lettura, rendendo possibile invece la scrittura, sbloccandola unicamente quando la generazione dell’identificativo fosse avvenuta correttamente. Ciò è stato necessario per evitare appunto che ci fosse il caso in cui più persone avessero lo stesso identificativo per la notizia appena inserita. Non sarebbe poi stato possibile recuperare univocamente la notizia dal suo ID. La stringa di generazione del numero successivo è molto facile in quanto si tratta di fare un update che incrementa il valore presente nella tabella e poi viene invocata una query per recuperare tale valore. In Oracle non è necessario bloccare/sbloccare le tabelle, poiché le sequences sono di per sé auto-bloccanti/sbloccanti.
Campo | Tipo | NULL | Chiave | Default |
ID | int(11) |
|
| 0 |
Tabella 4
Connection conn = connectDB();
long id_next;
String querySeq="";
if (isOracle==true) { // Oracle
querySeq = "select RSS_SEQ.nextval id from DUAL";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(querySeq);
rs.next();
id_next = rs.getLong("id");
} else // MySQL
{
ResultSet rs= null;
PreparedStatement query;
lockTable(conn,"RSS_SEQ_MySQL");
querySeq = "UPDATE RSS_SEQ_MySQL SET id=LAST_INSERT_ID(id+1);";
PreparedStatement stmt = conn.prepareStatement(querySeq);
stmt.execute();
query=conn.prepareStatement("select * from RSS_SEQ_MySQL;");
rs = query.executeQuery();
rs.next();
id_next = rs.getLong("id");
unlockTable(conn);
}
Ovviamente per generare un ID con MySQL potevamo far uso di un campo auto-incrementante ma questo avrebbe complicato il codice per mantenere la compatibilità con i due differenti database.
Come abbiamo accennato in apertura, lo scopo del progetto era quello di realizzare un’applicazione di gestione delle notizie dei vari uffici del Comune. E’ logico pensare che ci siano dei diritti in base ai quali gli utenti possono accedere sia alla nostra applicazione sia anche ad un certo sottoinsieme di aree di interesse riguardanti le notizie. Per far questo, abbiamo dovuto definire il nome del gruppo, l’username e la password degli utenti che potevano accedere all’application server ed in particolare alle varie parti della procedura. Vediamo cosa abbiamo modificato per poter far funzionare l’applicazione con Tomcat. Per definire il nome dei gruppi e/o degli utenti, è necessario modificare il file tomcat-users.xml che si trova nella directory “<INSTALL_DIR>/conf” (nel nostro caso tale file si trova in “/usr/local/tomcat/jakarta-tomcat-5.5.4/conf”)
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="rss"/>
<user username="utente" password="5678" roles="rss"/>
</tomcat-users>
Per OC4J il file che definisce i gruppi e le login di accesso degli utenti si trova in “<INSTALL_DIR>/j2ee/home/config“ (che per noi è in “/usr/local/tomcat/oc4j1013/j2ee/home/config”) e si chiama principals.xml (o jazn-data.xml se vogliamo utilizzare JAZN per l’autenticazione/autorizzazione). Abbiamo deciso di inserire le login nel file principals.xml, modificando così tale file:
<group name="rss">
<description>RSS</description>
<permission name="rmi:login" />
</group>
<user username="utente" password="5678" >
<description>Utente di prova - ha solo RSS</description>
<group-membership group="rss" />
</user>
Va però detto all’OC4J il file da cui leggere le informazioni per la login e questo lo facciamo modificando, sempre nella stessa directory, il file chiamato application.xml come di seguito riportato:
<!-- jazn provider="XML" location="./jazn-data.xml" / -->
<principals path="./principals.xml" />
JAZN offre una sicurezza maggiore rispetto all’utilizzo del semplice file principals.xml anche perché utilizza le password codificate, al contrario di come invece abbiamo scelto di fare noi. Una volta finito di definire i gruppi ed i ruoli, bisogna modificare anche il file web.xml che si trova all’interno del file WAR (o EAR) nella directory /WEB-INF. Qui va specificato il metodo di autenticazione. Abbiamo scelto il tipo “BASIC” che autentica l’utente attraverso l’username e la password ottenuti da un client Web. Purtroppo questa autenticazione non è particolarmente sicura perché invia i dati di login in chiaro. Non è di certo la scelta migliore, ma abbiamo ritenuto che tale progetto non necessitasse di un livello di sicurezza maggiore, non contenendo dati sensibili. Un’altra scelta per l’autenticazione è quella di tipo “Form-based”, in cui è possibile personalizzare la schermata di login e della pagina di errore che vengono presentate all’utente tramite un browser HTTP. Questo tipo di autenticazione, come quella BASIC, non è particolarmente sicura, poiché il contenuto della finestra di dialogo è inviato come testo e il server destinatario non è autenticato. La sicurezza maggiore la otteniamo invece utilizzando il metodo “Client-Certificate” perché fa uso di “HTTP over SSL”, in cui il server e, opzionalmente, il client si autenticano reciprocamente utilizzando certificati a chiave pubblica. Il Server Socket Layer (SSL) permette la codifica dei dati, l’autenticazione server, l’integrità dei messaggi, e l’opzionale autenticazione client per una connessione TCP/IP. Il certificato a chiave pubblica è l’equivalente digitale di un passaporto ed è rilasciato da un’organizzazione che fa da garante. In questo tipo di autenticazione il Web server autentica il client attraverso il certificato X.509 del client, un certificato a chiave pubblica conforme allo standard X.509.
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<security-role>
<description>Tutti gli utenti che accedono ai feed</description>
<role-name>rss</role-name>
</security-role>
Sempre all’interno del nostro file web.xml dobbiamo poi indicare a quali parti del progetto si può accedere una volta autenticati e quali gruppi di utenti vi possono accedere.
<security-constraint>
<web-resource-collection>
<web-resource-name>rss</web-resource-name>
<url-pattern>/faces/protectRSS/*</url-pattern>
<url-pattern>/faces/menu.jsp</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>rss</role-name>
</auth-constraint>
</security-constraint>
Così facendo, ogni volta che qualcuno tenta di accedere all’applicazione montata su Tomcat o su OC4J, appare una finestra di login in cui inserire l’username e la password e, una volta autenticati, si può accedere alla gestione vera e propria delle notizie. E’ possibile definire un sottoinsieme di aree di interesse riguardanti le notizie, per farlo abbiamo creato una tabella chiamata “RSS_DIRITTI_UTENTI” (Tabella 5) in cui andiamo a memorizzare l’username e le sue aree di competenza per la gestione delle notizie. Nessuno di questi campi può assumere un valore nullo.
Campo | Tipo | NULL | Chiave | Default |
ID | int(11) |
| PRI | 0 |
UTENTE | varchar(50) |
|
|
|
DIRITTO_TIPO_NOTIZIA | int(5) |
|
| 0 |
Tabella 5
Abbiamo poi creato un’altra tabella denominata “AREA” (Tabella 6) che contiene la lista degli uffici o direzioni che possono inserire le notizie.
Campo | Tipo | NULL | Chiave | Default |
DU_CODICE | varchar(5) |
|
|
|
DU_DESCR | varchar(50) |
|
|
|
Tabella 6
Ogni volta che l’applicazione è eseguita, viene fatta una chiamata ad un metodo che si occupa di popolare l’oggetto direzioni che è un ArrayList. Tale oggetto appartiene alla classe java.util.ArrayList ed è simile al Vector, fatta eccezione per il fatto che non è una struttura dati sincronizzata (lo è invece il Vector). Questo significa che se threads multipli accedono alla struttura e almeno uno ne varia la dimensione, allora è necessario sincronizzare la lista esternamente. Per far ciò si deve effettuare la sincronizzazione sull’oggetto che incapsula la lista: se tale oggetto non esiste, questa deve essere racchiusa usando il metodo Collections.synchronizedList appartenente alla classe java.util.Collections e ciò avviene in fase di creazione della lista, ad esempio nel seguente modo:
List list = Collections.synchronizedList(new ArrayList(...));
Torniamo alla lista direzioni e vediamo come siamo riusciti a recuperare gli ambiti di appartenenza di ogni utente abilitato ad accedere all’applicazione. Innanzi tutto bisogna chiamare il metodo readTable, passandogli come parametri il nome della tabella, la colonna che identifica l’ID (DU_CODICE), la descrizione (DU_DESCR) ed una stringa che descrive i diritti associati all’utente che ha effettuato l’autenticazione. Questa stringa viene generata dal metodo diritti che riceve in ingresso i codici delle direzioni a cui l’utente può accedere.
direzioni = db.readTable("area","du_codice","du_descr",db.diritti(db.dirittiUtente(utenteLoggato),"du_codice"));
Questa ArrayList viene popolata mediante l’uso di alcuni metodi che appartengono alla classe it.grosseto.comune.rss.DB ed hanno il compito di creare la lista delle direzioni su cui possiamo andare a scrivere le notizie, partendo da una lista composta unicamente da numeri, rappresentanti i codici degli uffici. Vi è anche la possibilità di definire un “superutente”, che può gestire tutti i tipi di notizie dei vari uffici e questo semplicemente associando per l’username che ci interessa, il valore “0” nel campo DIRITTO_TIPO_NOTIZIA della tabella “RSS_DIRITTI_UTENTI” (Tabella 5).
public ArrayList readTable(String tabella, String colonnaID, String colonnaDescr, String where) throws Exception
{
ArrayList risultati = new ArrayList();
if (where==null) {where="";}
if (!where.equals(""))
{
where = "where " + where;
}
if (colonnaDescr==null) {colonnaDescr="";}
if (colonnaID==null) {colonnaID="";}
if (colonnaDescr.equals("") || colonnaID.equals("")) { return risultati=null; }
Connection conn = connectDB();
ResultSet rs= null;
String riga_query = "select " + colonnaID + ", " + colonnaDescr + " from " + tabella + " " +where+ " order by "+colonnaDescr;
PreparedStatement query=conn.prepareStatement(riga_query);
rs = query.executeQuery();
while (rs.next())
{
risultati.add(new SelectItem(rs.getString(colonnaID), rs.getString(colonnaDescr), ""));
}
rs.close();
conn.close();
return risultati;
} // readTable
public Vector dirittiUtente(String utente) throws Exception
{
Vector risultati = new Vector();
Connection conn = connectDB();
ResultSet rs= null;
PreparedStatement query=conn.prepareStatement("select * from rss_diritti_utenti where utente = ?");
query.setString(1,utente);
rs = query.executeQuery();
while (rs.next())
{
risultati.add((new Integer(rs.getInt("diritto_tipo_notizia"))));
}
rs.close();
conn.close();
return risultati;
}
public String diritti(Vector rights, String campo)
{
if (campo == "") { return null; }
String riga=campo+"=null";
String dato;
int max=rights.size();
if ( max>0 )
{
dato=(rights.get(0)).toString();
if (dato.compareTo("0") == 0) { riga=""; }
else riga="("+campo+"="+dato+")";
for (int i=1;i<max;i++)
{
dato=(rights.get(i)).toString();
if (dato.compareTo("0") == 0)
{
riga="";
break;
}
else riga=riga+" or ("+campo+"="+dato+")";
}
}
return riga;
}
Concludiamo la descrizione del progetto parlando di come abbiamo recuperato l’username dell’utente che ha effettuato l’autenticazione. Per far ciò, abbiamo scelto di usare il metodo getRemoteUser della classe javax.faces.context.ExternalContext che consente alle API di Faces di ignorare la natura dell’ambiente che contiene l’applicazione. In particolare questa classe consente alle applicazioni basate su JavaServer Faces di essere eseguite sia in Servlet sia in Portlet (componenti web utilizzabili come plug-in e gestite da un qualche container per la realizzazione e costruzione di portali Enterprise personalizzati). Una volta ottenuta l’ExternalContext basta richiamare il metodo adatto e abbiamo così l’username della persona che ha fatto l’accesso alla nostra applicazione.
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
String remoteUser = externalContext.getRemoteUser();
utenteLoggato=remoteUser;
Come nota conclusiva, possiamo affermare che sarebbe stato più facile fare un’applicazione che fosse nata per essere eseguita unicamente sotto OC4J o Tomcat o altro application server oppure che sfruttasse pienamente le potenzialità del database Oracle o di MySQL o di qualsiasi altro database. Non è stato semplicissimo conciliare il mondo Open Source con quello commerciale e per farlo abbiamo attuate delle scelte. Quale di queste due strade sia quella da intraprendere per il momento non è chiara, ciascuno di questi due mondi ha i suoi pro e i suoi contro. L’unica cosa che possiamo fare per il momento, è vedere come si evolverà il mondo informatico e regolarci di conseguenza, seguendo la strada che verrà ritenuta più affidabile in termini di praticità e di funzionalità.