Con il web 2.0 l'utente è passato dallo stato di semplice fruitore a quello di di publisher. Ciò implica che chi sviluppa applicazioni web deve mettere a disposizione dell'utente gli strumenti adatti a tale scopo.
Nell'articolo vediamo come si fa a gestire una directory da una applicazione Java, in cui gli utenti possono pubblicare le proprie risorse, sotto forma di file.
La nostra semplice applicazione si occuperà di gestire la creazione di account utente, quindi permettere l'accesso attraverso le funzioni di login (per le operazioni di scrittura, non per quelle di lettura):
- Creazione account
- Eliminazione account
- Inserimento file (upload)
- Creazione directory
- Lettura directory
- Lettura file
- Login
Un utente, attraverso le pagine della web application, potrà iscriversi al servizio, o cancellarsi. Potrà creare delle directory a partire dal proprio spazio riservato ed inserire dei file.
L'operazione di lettura della directory e di apertura dei file (download) sono invece accessibili a chiunque.
Dati
Vista la relativa semplicità dell'applicazione non abbiamo bisogno di gestire grosse quantità di dati, giusto una tabella che rappresenta l'utente e le proprietà che dobbiamo rendere persistenti.
Listato 1. Creazione database e tabella utenti
//Script.sql
CREATE DATABASE 'filewebapp';
DATABASE 'filewebapp';
CREATE TABLE 'user' (
'uid' varchar(255) NOT NULL default '',
'password' varchar(16) NOT NULL default '',
'email' varchar(255) NOT NULL default '',
'disk' int(11) NOT NULL default '50',
PRIMARY KEY ('uid')
);
Logica applicativa
Implementiamo il tutto incapsulando la logica in due classi, una per la persistenza dei dati (accesso al database), l'altra per la gestione dei file.
Partiamo quindi "dal basso" creando le interfacce per la gestione dei dati e dei file, rispettivamente UserManager e FileManager.
Listato 2. Gestore degli utenti
//UserManager.javapackage
it.html.user;
import java.sql.SQLException;
/**
* @author Pasquale Congiustì
* La gestione della persistenza è affidata a questa classe
*/
public interface UserManager {
//Creazione di un account
public void create(String user,String password,String email) throws SQLException;
//Eliminazione di un account
public void delete(String user)throws SQLException;
//Operazione di login
public User login(String user,String password)throws LoginException,SQLException;
}
Come si vede, il gestore degli utenti prevede tre dei requisiti prima citati: creazione dell'account, eliminazione, login. Tutte le operazioni che riguardano l'utente e gli aspetti di persistenza (anche il login necessita di accedere alle informazioni persistenti) sono definite in questa interfaccia.
Listato 3. Gestore dei file
//FileManager.javapackage
it.html.file;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
/**
* @author Pasquale Congiustì
* La gestione dei file viene delegata qui
*/
public interface FileManager {
//Creazione di una directory DIR per l'utente USER
public void makeDir(String user,String dir)throws IOException;
//Creazione di una directory root per l'utente
public void createAccount(String user)throws IOException;
//Creazione dei file dell'utente USER nella directory DIR
public void insert(String user,String dir, Collection file)throws Exception, NoMoreSpaceException;
//Eliminazione del file FILENAME dell'utente USER nella directory DIR
public void delete(String user,String filename, String dir)throws FileNotFoundException;
//Creazione della lista di file presenti nella directory DIR per l'utente USER
public File[] getFileList(String user,String dir)throws IOException;
}
Gli altri requisiti sono invece completati attraverso i metodi previsti dal gestore dei file, che si occuperà di gestire la struttura di directory e di inserire o eliminare i file sulle indicazioni dell'utente.
Queste interfacce saranno utilizzate da un oggetto controller, una servlet, che si occuperà di creare le classi concrete (che adesso vedremo) seguendo il flusso di controllo atteso dalle diverse funzioni. In questo modo si può intervenire sul processo in maniera indipendente dai dati. Se ad esempio, vogliamo mantenere traccia di tutte le operazioni effettuate dall'utente potremo farlo modificando la relativa funzione nel punto opportuno.
Vediamo un pezzo di UserManagerConcrete
che realizza UserManager
: in questo caso è stata pensata per la persistenza su database (per cui utilizzeremo JDBC).
Listato 4. Gestisce l'autenticazione degli utenti (Codice completo)
//UserManagerConcrete.javapackage
it.html.user;
import java.sql.Connection;
..//
public class UserManagerConcrete implements UserManager {
//La classe attraverso la quale gestiamo l'accesso al database
private Connection conn;
public UserManagerConcrete(Connection conn){
this.conn=conn;
}
public User login(String user, String password) throws LoginException, SQLException {
User toRet=null;
String sql="SELECT * FROM user where uid='"+user+"' AND password='"+password+"'";
Statement st=conn.createStatement();
ResultSet rs=st.executeQuery(sql);
if (rs.next()){
//Se user e password coincidono, l'utente esiste
..//
}
//Chiusura dei flussi aperti
..//
if (toRet==null){
//Nessun utente è stato trovato!
..//
}
return toRet;
}
}
L'idea è quella di restituire un oggetto User (un Javabean con le proprietà valorizzate) o un'eccezione, se il login fallisce.
Il caso in questione mostra una sola eccezione, generica, relativa al login fallito. Ciò implica che il controller, avrà un blocco try-catch con un solo flusso eccezionale.
Gli altri due metodi della classe ricalcano la logica del metodo login: definizione di una query sulla base dei parametri ed avvio della stessa secondo la logica JDBC.
Se il gestore degli utenti si appoggiava alle librerie JDBC, il gestore dei file farà uso diretto delle librerie java.io ed in particolare farà uso della virtualizzazione di File.
Listato 5. Incapsula le operazioni effettuate sul file system (Codice completo)
//FileManagerConcrete.javapackage
it.html.file;
import java.io.File;
..//
public class FileManagerConcrete implements FileManager {
//Tutte le operazioni vengono effettuate a partire da questa directory
private String root;
public FileManagerConcrete(String root){
this.root=root;
}
public void createAccount(String user) throws IOException {
//Creazione del path
//Creazione della directory sul disco (lato server)
}
public void insert(String user, String dir, Collection items) throws Exception {
//Creazione del path
String directory=root+File.separator+user;
//se DIR è vuoto, ci troviamo nella directory root
if (dir!=null && !dir.equals(""))
directory+=File.separator+dir;
//Iteriamo sulla lista
Iterator it = items.iterator();
//Per ogni item, salviamo un file
..//
}
}
public File[] getFileList(String user, String dir) throws IOException {
//Creazione del path
String directory=root+File.separator+user;
//se DIR è vuoto, ci troviamo nella directory root
if (dir!=null && !dir.equals(""))
directory+=File.separator+dir;
File f=new File(directory);
return f.listfile();
}
}
Il costruttore si aspetta una directory, a partire dalla quale verrà creato l'albero di tutte le directory. In questa applicazione (come si verà nella servlet) tale logica prevede la creazione di una directory nominata public, sotto la quale verrà inserita una directory con il nome dello user che l'ha creata (vedi il metodo createAccount).
L'esigenza di creare la root a partire dalla web application è necessaria per poter referenziare attraverso il browser i documenti ed eseguirne il download automatico.
L'upload dei file è la cosa più complessa di questa applicazione. A questo scopo abbiamo utilizzato il progetto opensource Jakarta FileUpload che, attraverso l'oggetto FileItem, virtualizza il file che l'utente vuole uploadare.
Gli altri metodi (nello spezzone di codice abbiamo visto createAccount()
e getFileList()
) ricalcano le operazioni messe a disposizione dalla classe java.io.File (creazione di una directory e recupero di un array di file).
Per aiutare l'utente nella fase di apprendimento, dividiamo l'articolo in due pezzi: con il primo, fino a questo punto, abbiamo visto una panoramica delle operazioni di base; nei prossimi giorni pubblicheremo la seconda parte nella quale andremo ad operare sul server.
Controller
Dopo aver visto la panoramica delle operazioni di base (prima parte di questo articolo) è il momento di metterle assieme. Di questo si occupa la servlet, oltre alla gestione dei privilegi di accesso per le operazioni di scrittura.
Listato 6. Mette assieme le operazioni base (Codice completo)
//Dispatcher.javapackage
it.html.controller;
..//
public class Dispatcher extends HttpServlet {
//Questo oggetto virtualizza l'accesso al database
..//
public void init(ServletConfig conf)throws ServletException{
super.init(conf);
String class_driver="org.gjt.mm.mysql.Driver";
String url_addr="jdbc:mysql://127.0.0.1:3306/filewebapp?user=root&password=xyz";
dbConn=new DBConnection(class_driver,url_addr);
//Apertura della connessione
..//
//Recuperiamo il path assoluto del web container, che sarà la base del FileManager
..//
}
public void destroy(){
//Chiusura della connessione
dbConn.closeDBConnection();
}
//Useremo questo metodo come dispatcher in base al parametro op
//creando un metodo per ogni funzione richiesta
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
String op=req.getParameter("op");
//Creazione di un account
..//
//Operazione di login
..//
}
}
Il ciclo di vita della servlet prevede una sua inizializzazione (metodo init()
) quando questa viene creata. In qualche modo possiamo dire che questo metodo è una sorta di "costruttore" della classe. Qui dentro ci preoccupiamo di istanziare tutte quelle classi che ci serviranno durante l'esecuzione della classe. La fine della servlet è invece segnata dalla chiamata destroy()
dove vengono chiuse le connessioni aperte.
A supporto delle operazioni di connessione al database è stata creata una classe che si occupa di caricare il driver necessario (nel caso di esempio un driver MySQL) e di collegarsi al database. Questa classe, restituisce un oggetto Connection che verrà passato allo UserManager.
La creazione del FileManager, invece, dipende dalla directory di root che, attraverso il metodo getRealPath()
di ServletContext restituirà la directory base della web application.
Senza entrare nel dettaglio di tutte le operazioni, vediamo quelle che più direttamente riguardano la gestione dei file.
Listato 7. Operazioni per la gestione dei file (Codice completo)
//Dispatcher.java
private void doCreate(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//Recupero dei parametri attesi ed esecuzione della funzione
..//
}
private void doUpload(HttpServletRequest req, HttpServletResponse resp) throws IOException{
String user=req.getParameter("user");
String dir=req.getParameter("dir");
//Verifica che l'utente sia autenticato
..//
//Deleghiamo la creazione del file lato server alla libreria Apache Upload
boolean isMultipart = FileUpload.isMultipartContent(req);
if (isMultipart) try {
DiskFileUpload upload = new DiskFileUpload();
//Recuperiamo la lista di file da copiare
Collection file = upload.parseRequest(req);
fm.insert(user,dir,file);
resp.getWriter().println("Il file è stato copiato sul server.");
} catch (IOException e) {
..//
}
}
private void doShowfile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//Recupero dei parametri attesi ed esecuzione della funzione
..//
try {
File[] file=fm.getFileList(user,dir);
//Salviamo l'oggetto nella request, in modo da renderli visibili
RequestDispatcher rd=this.getServletContext().getRequestDispatcher("/list.jsp");
req.setAttribute("file",file);
//alla pagina JSP che verrà inoltrata
rd.forward(req,resp);
} catch (IOException e) {
//Gestione flusso eccezionale
resp.getWriter().println("Si è verificata un'eccezione, prego riprovare.");
}
}
..//
Il metodo di creazione dell'account prevede il recupero dei parametri (user, password,email) e la chiamata alle relative funzioni UserManager.create(user,password,email)
e FileManager.createAccount(user)
.
Infine, attraverso il metodo getWriter()
, mostriamo un messaggio all'utente (di conferma o di errore).
Tramite il metodo DiskFileUpload.parseRequest() viene creata una lista di FileItem, che abbiamo visto rappresentare la virtualizzazione dei file da caricare: attraverso questi risulta facile convertirli in file sul server.
Views
La pagina list.jsp ha lo scopo di mostrare il contenuto di una directory e di effettuare le funzioni di upload di un file o di creazione di una nuova directory.
Listato 8. JSP che mostra il contenuto di una directory ed effettua le funzioni di upload (Codice completo)
//list.jsp
<%
//Costruzione della directory sotto la quale ci troviamo
String dir="public/"+request.getParameter("user");
if(!request.getParameter("dir").equals(""))
dir+="/"+request.getParameter("dir");
%>
..//
<h1>Lista file:<%=dir%></h1>
<p><%
java.io.File[] file=(java.io.File[])request.getAttribute("file");
if (file!=null){
for(int i=0;i<file.length;i++){
String symbol="-";
String href=dir+"/"+file[i].getName();
//Iterazione della lista di file
if (file[i].isDirectory()){
//Se è una directory il simbolo è +
//e il link (href) punta alla funzione di visualizzazione
symbol="+";
String directory=file[i].getName();
if(!request.getParameter("dir").equals("")){
directory=request.getParameter("dir")+"/"+directory;
href="action?op=show&user="+request.getParameter("user")+"&dir="+directory;
}
}
%>
//Mostro una linea composta dal simbolo (+, per le directory)
//e il nome del file (linkato alla specifica risorsa, o directory)
<div>
<%=symbol%>
<a href="<%=href%>"><%=file[i].getName()%></a>
</div>
<%
}
}
%>
</p>
..//
La visualizzazione degli elementi dell'array file, segue questo schema: se il file passato è una directory, allora il link deve puntare alla funzione di visualizzazione di quella directory (sul click si aprirà la pagina che ne mostra i suoi elementi), altrimenti linka alla risorsa (sul click avverrà l'operazione di download).
Listato 9. Form per inserimento file e creazione directory
...//
<p>Inserisci file (in questa directory)<br/>
<form enctype='multipart/form-data' method='POST'
action='action?op=upload&user=<%=request.getParameter("user")%>&dir=<%=request.getParameter("dir")%>'>
<input type="file" name="file">
<input type="submit" value="Upload">
</form></p>
<p>Crea directory (da questa directory)<br/>
<form method='POST' action='action'>
<input type="hidden" name="op" value="md">
<input type="hidden" name="user" value="<%=request.getParameter("user")%>">
<input type="text" name="dir" value="<%=request.getParameter("dir")%>">
<input type="submit" value="Inserisci">
</form></p>
..//
Cliccando sui file, sia avvia il download dal server del documento richiesto, mentre cliccando sulle directory si visualizza, ricorsivamente, il loro contenuto.
Installazione e conclusioni
Per poter installare l'esempio completo, sarà necessario creare un database e portare i relativi driver (quelli per MySQL li trovate nel package allegato) sotto il classpath della web application (WEB-INF/lib).
Sempre sotto il classpath dovranno essere inserite le due librerie del progetto Jakarta Upload.