Come caricare un file sul server in AJAX

Postato da ROb | nella categoria Sviluppo web | martedì, 2 marzo 2010

2

I form html tradizionali negli anni hanno dimostrato numerosi limiti. Quelli più notevoli erano impossibilità di effettuare validazione server side, impossibilità di gestire dei campi autopopolati durante la digitazione, post completo del form senza possibilità di parzializzazione.

Ajax file upload

Tutte queste cose hanno reso il web uno strumento difficilmente utilizzabile per applicazioni che richiedessero un’elevata interazione con l’utente.
Negli ultimi anni AJAX ha rivoluzionato i tradizionali form html permettendo ai programmatori di poter dialogare agilmente con il loro backend e permettendo agli utenti di godere una più dinamica esperienza web.

Recentemente ho avuto la necessità di gestire un campo file in un form ajax. Per form ajax intendo un form che invia i suoi dati serializzati al server e non fa il post dell’intera pagina.
La tecnologia utilizzata era jQuery così ho cercato un plugin che si integrasse bene con tale tecnologia e ho trovato AJAX Upload.

Inseriamo innanzitutto gli script javascript nell’header necessari alla definizione di jQuery e del plugin AJAX Upload:

...
<head>
...
<script type="text/javascript" src="js/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="js/ajaxupload.js"></script>
...
</head>
...

A questo punto bisogna creare il div che cliccato avvierà la procedura di caricamento file:

<div id="upload_button">Upload</div>

Infine dobbiamo inizializzare i campi di tipo file all’interno del nostro form:

$(document).ready(function() {
    new AjaxUpload('upload_button_id', {action: 'uploadServletPath'});
}

Al rendering della pagina cliccando sul div upload_button si aprirà la finestra di dialogo per la scelta del file e il file sarà direttamente inviato via POST (senza però il POST dell’intera pagina) alla servlet di upload che abbiamo specificato nell’inizializzazione del componente.

Nel costruttore di inizializzazione possiamo anche passare altri parametri di configurazione e gestire alcuni eventi precedenti (o successivi) al caricamento utili per valorizzare altre variabili del form o per visualizzare la progressione del caricamento o il nome del file caricato sul server.
E’ anche possibile stabilire il tipo di risposta ricevuta dal server: html, xml o json.

Per chi fosse interessato ecco il codice Java necessario al salvataggio del file:

/*
 *	author: Roberto Rossi
 */
package it.bits4beats.servlets;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.*;
import java.util.Iterator;
import java.util.List;

public class UploadServlet extends HttpServlet {
	private static Log log = LogFactory.getLog(UploadServlet.class);
	public static final String DEFAULT_UPLOAD_DIR_INIT_PARAM = "DefaultUploadDir";

	/**
	 *
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * This is a default constructor for UploadServlet class.
	 *
	 */
	public UploadServlet(){
	}

	public void init() throws ServletException {
		super.init();
	}

	/**
	 * Receive a file to be uploaded and save it into filesystem
	 */
	public void doGet(HttpServletRequest request,
			HttpServletResponse response)
	throws ServletException,java.io.IOException {
		log.info("method not supported!");
	}

	public void doPost(HttpServletRequest request,
			HttpServletResponse response)
	throws ServletException,java.io.IOException {

		String defaultUploadDir = getServletConfig().getInitParameter(DEFAULT_UPLOAD_DIR_INIT_PARAM);
		if (defaultUploadDir != null && !defaultUploadDir.equals("")) {
			log.debug("> default upload dir: " + defaultUploadDir);
		}

		/* the uploading user */
		String username = request.getParameter("username");
		/* max size of file upload */
		String sizeParam = request.getParameter("sz");
		Long maxBytes = null;
		if (sizeParam != null && !sizeParam.isEmpty()) {
			try {
				/* max 5Mb file uploaded */
				maxBytes = new Long(5242880);
			}
			catch (NumberFormatException nfe) {
				/* do nothing */
			}
		}

		log.debug("remote user: " + request.getRemoteUser());
		log.debug("user principal: " + (request.getUserPrincipal() == null ? "" : request.getUserPrincipal().getName()) );
		log.debug("username: " + username);

		String status = "true";
		String message = "";
		String savedfilename = "";
		String attachmentname = "";
		String originalFilename = "";

		/* reading file streams */
		boolean isMultipart = ServletFileUpload.isMultipartContent(request);

		if (isMultipart) {

			FileItemFactory factory = new DiskFileItemFactory();
			ServletFileUpload upload = new ServletFileUpload(factory);
			List items = null;
			try {
				items = upload.parseRequest(request);

				if (items != null && items.size() > 0) {

					File uploadDirFile = null;
					String uploadDirFileRelativePath = null;

					uploadDirFile = new File(getServletContext().getRealPath(defaultUploadDir));
					uploadDirFileRelativePath = defaultUploadDir;

					Iterator itr = items.iterator();

					FileItem fileItem = null;
					FileItem filenameItem = null;

					while(itr.hasNext()) {
						FileItem item = (FileItem) itr.next();

						if (!item.isFormField()) {
							fileItem = item;
						}
						else {
							if (item.getFieldName().equals("attachmentFilename")) {
								filenameItem = item;
								log.debug("filenameItem name: " + filenameItem.getFieldName() + " valore: " + filenameItem.getString());
							}
						}
					}

					if (fileItem != null) {
						/* there is a file item in the request */
						String fileName = fileItem.getName();
						log.debug("fileItem name: " + fileName);

						if (fileItem.getName().contains("\\")) {
							log.debug("Windows file");

							/* windows file path */
							fileName = fileItem.getName().substring(fileItem.getName().lastIndexOf("\\") +1);
							log.debug("fileItem name: " + fileName);
						}

						File fullFile  = new File(fileName);
						File savedFile = new File(uploadDirFile, fileName);

						if (savedFile != null) {

							/* check file size */
							if (maxBytes != null) {
								if (fileItem.getSize() > maxBytes.longValue()) {
									FileTooBigException ftbe = new FileTooBigException();
									ftbe.setMaxSize(new Long(maxBytes.longValue()/1024/1024).toString());
									throw ftbe;
								}
							}

							if (savedFile.exists()) {
								/* we need to create a file variant, save it and send it to the user */
								String fileNameWithoutExtension = savedFile.getName().substring(0, savedFile.getName().lastIndexOf("."));
								originalFilename = fileNameWithoutExtension;
								String suffix = savedFile.getName().substring(savedFile.getName().lastIndexOf(".") + 1);

								File newFile = File.createTempFile(fileNameWithoutExtension + "_", "." + suffix, uploadDirFile);
								fileItem.write(newFile);

								log.debug("newFile: " + newFile.getAbsolutePath());
								status = "true";
								message = "OK " + newFile.length() + " byte salvati";

								/* relative file path */
								savedfilename = uploadDirFileRelativePath + newFile.getName();
								String filenameWithExt =  extractFilenameWithExtensionFromPath(savedfilename);
								attachmentname = extractFilename(filenameWithExt);
							}
							else {
								log.debug("writing file to: " + savedFile.getAbsolutePath());
								originalFilename = extractFilename(extractFilenameWithExtensionFromPath(savedFile.getAbsolutePath()));
								fileItem.write(savedFile);

								status = "true";
								message = "OK " + savedFile.length() + " byte salvati";

								/* relative file path */
								savedfilename = uploadDirFileRelativePath + fullFile.getName();
								String filenameWithExt =  extractFilenameWithExtensionFromPath(savedfilename);
								attachmentname = extractFilename(filenameWithExt);
							}
						}
						else {
							log.error("cannot create file");
							status = "false";
							message = "Errore generico di salvataggio";
						}
					}

				}
				else {
					log.debug("no item to be saved");
				}
			}
			catch (FileTooBigException ftbe) {
				status = "false";
				message = "File troppo grande, dimensione massima (Mb): " + ftbe.getMaxSize();
			}
			catch (Exception e1) {
				e1.printStackTrace();
				log.error("error in uploading");

				status = "false";
				message = "Errore generico di salvataggio";
			}
		}

		PrintWriter out = response.getWriter();

		String responseMessage = "{\"success\":" + status + ",\"message\":\"" + message + "\",\"savedfilename\":\"" + savedfilename + "\",\"attachmentname\":\"" + attachmentname + "\",\"originalfilename\":\"" + originalFilename + "\"}";
		log.debug("responseMessage: " + responseMessage);
	    out.println(responseMessage);
	}

	/**
	 * This method extract filename (with extension) from filepath
	 *
	 * @param filepath
	 * @return filename without extension
	 */
	public static String extractFilenameWithExtensionFromPath(String filepath) {
		if (filepath != null) {
			int index = filepath.lastIndexOf("/");

			if (index > 0 && index < (filepath.length() -1)) {
				return filepath.substring(index + 1);
			}
			else return null;
		}
		else {
			return null;
		}
	}

	/**
	 * This method extract filename (without extension) from filename with extension
	 *
	 * @param filepath
	 * @return filename without extension
	 */
	public static String extractFilename(String filenameWithExtension) {
		if (filenameWithExtension != null) {
			int index = filenameWithExtension.lastIndexOf(".");

			if (index > 0) {
				return filenameWithExtension.substring(0, index);
			}
			else return filenameWithExtension;
		}
		else {
			return null;
		}
	}

	class FileTooBigException extends Exception {
		private static final long serialVersionUID = 1L;

		private String maxSize = null;

		public String getMaxSize() {
			return maxSize;
		}

		public void setMaxSize(String maxSize) {
			this.maxSize = maxSize;
		}

		public FileTooBigException() {
			super();
		}

		public FileTooBigException(String message) {
			super(message);
		}
	}

}

Nel sito del plugin trovate anche una demo completa e funzionante di AJAX upload.

ExtJS: il framework di sviluppo Javascript per eccellenza

Postato da ROb | nella categoria Sviluppo web | venerdì, 12 febbraio 2010

2

Recentemente ho avuto il bisogno di riscrivere completamente un’applicazione web.
Penso di conoscere bene html, css e javascript ma, nonostante questo, nella scrittura di applicazioni web ho dovuto spesso risolvere gli stessi problemi, rispondere alle stesse esigenze e scrivere in diverse occasioni lo stesso codice.

ExtJS logo

Per questi motivi mi sono guardato un pochino attorno cercando una libreria Javascript che mettesse in mano dello sviluppatore un ambiente con oggetti pronti all’uso, semplici da configurare, carini da vedere e potenti allo stesso tempo.
Mi sono concentrato sulla ricerca di toolkit Javascript e non su altro perché avevo necessità di sfruttare tale libreria con differenti tecnologie server side.
Ovviamente un aspetto fondamentale di valutazione è che il toolkit funzioni con tutti i browser web più diffusi (Firefox, Safari e Chrome, Internet Explorer … IE è volutamente stato messo per ultimo).

Indubbiamente anche in questo caso il panorama non è assolutamente deludente e il mondo open source offre come al solito le sue superbe soluzioni.
I più famosi in quest’ambito sono: Dojo, Prototype, Scriptaculous, jQuery (anche se più che un toolkit è un nuovo approccio Javascript alla selezione degli oggetti) , SmartClient, ed ExtJS (anche nella sua variante Yui).

Dopo aver visto gli esempi proposti nel sito e aver fatto alcuni test ho provato a svilupparla con ExtJS.

Rotte le prime iniziali difficoltà per impostare l’ambiente con tutte le opportune dipendenze, mi sono subito accorto di avere a che fare con una libreria veramente matura, flessibile e molto performante.
Il forum della comunità è attivissimo, la documentazione (presentata anch’essa tramite un’applicazione web ExtJS) è comodissima da navigare e ben scritta, i bug sono pochi e di poco rilievo, perlomeno nella versione che ho provato e che sto ancora usando, cioè la 2.2.1 (il ramo attuale è alla major release 3.x).

Essendo una libreria già molto famosa, numerosi sono i tool di contorno alla libreria stessa. C’è chi l’ha usata per generare un template wordpress (http://extjswordpress.net/), chi l’ha sfruttata per generare un desktop virtual su web, chi ci ha scritto un software di collaborazione.

ExtJS desktop example

Lo showcase dimostrativo è veramente molto ricco e ci sono oggetti già pronti per qualsiasi situazione.
Gli widget che sto usando più di tutti sono le griglie (con la possibilità di caricare i dati in modo asincrono via JSON), i form, le finestre flottanti, i tab e gli accordion.
Con ExtJS è anche semplicissimo implementare caricamenti e callback via AJAX mettendo infatti a disposizione degli oggetti statici per l’invio e la ricezione dei dati serializzati.

Per gli amanti del Google Web Toolkit c’è anche una versione di ExtJS integrata con GWT chiamata GXT. Il suo sviluppo, pur se con qualche ritardo dovuto a una partenza successiva, sta procedendo bene e contano a breve di allineare le funzionalità ExtJS con GXT.

Dal punto di vista della licenza ExtJS ha adottato una coercizione “forte” di chi la utilizza verso l’open source basata sul principio del Quid Pro Quo.
Chi vuole utilizzare ExtJS per progetti open source è liberissimo di farlo chi invece intende utilizzarla per progetti non “open” deve acquistare una licenza commerciale.
In realtà fino alla versione 2.x la politica di licenza era leggermente diversa (LGPL) e permetteva di utilizzare la libreria anche in progetti closed source.

Fare debugging di applicazioni web Java con Tomcat ed Eclipse

Postato da ROb | nella categoria Java, Sviluppo web | mercoledì, 10 febbraio 2010

2

Qualche anno fa ritenevo impossibile fare debugging di applicazioni web.
Mi chiedevo in particolar modo come fosse possibile raggruppare all’interno di un unico debugger la componente client side (cioè il mix di codice html+css+javascript eseguito nel browser) con la componente server side (in qualsiasi forma si presentasse: php, java, dot.net).

Eclipse, compagno di debug

Ultimamente molti framework, così come importanti application server con il loro IDE associato, sono riusciti a colmare questa mancanza ed ad offire a noi poveri sviluppatori il nostro caro e provvidenziale debugger.

In realtà in questo articolo non spiegherò come sia possibile fare debugging di entrambe le componenti (client e server) in un’unico IDE bensì mi concentrerò solamente sulla parte server, che ho sempre considerato la più spinosa.
La parte client, in realtà, dà spesso meno noie. Il suo comportamento è più facilmente intuibile e spesso è sufficiente fare delle stampe extra di codice html o qualche alert javascript per poter venire a capo dei problemi (anche se non dobbiamo dimenticare l’utilissimo Firebug in Firefox, di cui ho parlato anche ieri, che permette di fare debugging anche del Javascript lato client).

Oggi voglio parlare del debugging server side di applicazioni web per il mondo Java e in particolare della modalità fornita da Eclipse chiamata “Remote Java Application”. E’ una modalità tale per cui è possibile avviare la JVM con un demone in ascolto su una determinata porta. A questa porta, IDE opportunamente istruiti (quale ad esempio Eclipse), possono collegarsi e dialogare con la JVM stessa.
La JVM segnala all’IDE la riga in quel momento in esecuzione e riceve dall’IDE stesso dei comandi in merito all’esecuzione del codice: breakpoint, step-into, step-over, stato delle variabili, …

Per arrivare a poter debuggare la nostra JVM remota, dobbiamo essenzialmente eseguire due passi: avviare la JVM di Tomcat con degli opportuni parametri per la modalità “remote instance” e indicare al nostro IDE, per il nostro progetto da debuggare, qual è la porta della JVM a cui collegarsi.

Cominciamo con la parte di modifica dell’avvio di Tomcat:

  • scegliere una porta libera su cui mettere in ascolto la JVM di Tomcat. Per questo esempio useremo la porta 5050
  • individuare il file startup.sh all’interno della nostra distribuzione Tomcat (la versione di Tomcat di questo post è la 6, ma tale procedura è ugualmente applicabile alla versione 5.5)
  • modificare l’ultima riga di tale file in questo modo
    # vecchia riga
    # exec "$PRGDIR"/"$EXECUTABLE" start "$@"
    # nuova riga
    export JPDA_TRANSPORT=dt_socket
    export JPDA_ADDRESS=5050
    exec "$PRGDIR"/"$EXECUTABLE" jpda start "$@"
    
  • controllare che, dopo aver avviato Tomcat, la JVM sia effettivamente in ascolto
    telnet localhost 5050
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    

Abbiamo terminato con le modifiche alla sequenza di avvio di Tomcat. Passiamo ora ad Eclipse.
Dopo aver scelto il progetto che vogliamo debuggare, le cui classi compilate devono essere in esecuzione all’interno di una webapp del nostro application server Tomcat, dobbiamo creare una nuova istanza di compilazione di tipo “Remote Java Application“.

  • dal menu “Run” di Eclipse scegliamo la voce: “Debug…”
  • scorriamo fino alla voce “Remote Java Application” e scegliamo “New” utilizzando il tasto destro del mouse


    Nuova istanza di debug JVM remota

  • diamo un nome alla nostra istanza di debug e modifichiamo la porta di default con la porta scelta 5050


    Modifica delle impostazioni standar, istanza remota java

  • a questo punto clicchiamo su Debug. Se tutto va bene Eclipse dovrebbe essere in grado di connettersi e dialogare con la nostro JVM in esecuzione con Tomcat

Ora viene la parte più bella!

Iniziamo a impostare dei breakpoint nei punti di codice che vogliamo controllare e facciamo sì che il flusso di esecuzione arrivi in quel punto.
Magicamente potremmo vedere tutti i nostri oggetti Java materializzarsi. Possiamo esaminarne tutte le proprietà, impostare delle espressioni di controllo, entrare e uscire dalle funzioni.

Un aspetto decisamente divertente è l’interazione con il browser. Interrompendo il flusso di esecuzione della JVM interrompiamo ovviamente anche la risposta del server verso il browser. Il cursore di attesa del nostro amato Firefox girerà finché non avremo terminato il controllo del nostro codice e lanceremo il nostro ultimo F8.

Nel caso di debug di applicazioni web AJAX tale possibilità di sviluppo facilita ulteriormente le cose. In questi casi infatti è meno banale controllare i dati restituiti dal server in quanto non compongono quasi mai una pagina html ispezionabile bensì solamente delle porzioni di codice XML o del Javascript serializzato in formato JSON.

Spero che l’articolo vi sia tornato utile; commenti e osservazioni sono sempre molto ben accetti.

Roundcube – libera webmail per le masse

Postato da ROb | nella categoria Linux, Sviluppo web, Utilità | domenica, 7 febbraio 2010

0

Questo è il motto che campeggia nell’homepage del sito di Roundcube.

Roundcube è un gradevolissimo, e a mio avviso anche molto valido, client web per la posta elettronica.
La cosa che più lo differenzia da altri applicativi open source dalle stesse funzionalità come SquirrelMail, Webmin o altri, è l’interfaccia utente che sfrutta appieno la tecnologia AJAX e che riesce ad offrire alll’utente un’esperienza di utilizzo molto piacevole.

Roundcube logo

L’installazione di Roundcube è in parte via riga di comando e in parte via web, tramite un apposito installer. E’ scritto in PHP e necessita di un database MySQL per il salvataggio delle impostazioni e delle preferenze degli utenti.

Ufficialmente ancora si trova in uno stato beta (la versione attuale è la 0.3.1) ma nell’utilizzo quotidiano non si riscontrano particolari malfunzionamenti.

Le feature più interessanti di Roundcube sono:

  • multilinguismo: le lingue supportate sono 65 (tra cui l’italiano)
  • drag-n-drop dei messaggi
  • composizione html delle email
  • rubrica integrata con funzionalità di ricerca
  • autocreazione delle utenze attraverso pattern di ricerca verso IMAP
  • gestione delle cartelle IMAP
  • possibilità di customizzazione del tema
  • gestione degli attachment

Interfaccia di Roundcube, pannello principale

La pagina principale ha un aspetto veramente ordinato e ricalca il layout tipico di un comune client di posta.
Nella parte sinistra ci sono le cartelle e nella parte destra i messaggi della cartella corrente e l’anteprima del messaggio selezionato, se attiva.

I bottoni in alto a destra attivano altri due importanti pannelli: la rubrica e le impostazioni.
Nella rubrica possiamo gestire i nostri contatti personali.
Il pannello delle impostazioni, invece, ci consente di sottoscrivere o de-sottoscrivere le cartelle IMAP della nostra mailbox, modificare il nome visualizzato associato al nostro indirizzo email e modificare le presenze di lettura e scrittura dei messaggi di posta.

Ci sono anche molti buoni propositi da parte del team di sviluppo per le prossime versioni.
Una delle cose più attese è senz’altro il pannello di amministrazione per la configurazione di Roundcube (dopo l’installazione è possibile intervenire solamente attraverso un file di configurazione), le scorciatoie da tastiera (per velocizzare l’utilizzo dell’applicazione sfruttando particolari combinazioni di tasti) e la preview degli attachment.

In bocca al lupo agli sviluppatori e un sentito grazie per il grande impegno!