sábado, 10 de noviembre de 2012

Como devolver un archivo con Apache Tapestry

Apache Tapestry
En esta entrada voy a explicar como enviar un archivo al cliente desde el servidor usando el framework Apache Tapestry. Esto es necesario si por ejemplo nuestra aplicación necesita generar un informe ya sea en formato txt, excel, pdf, un archivo comprimido o de cualquier otro tipo.

En tapestry el proceso no es muy complicado, tan solo deberemos devolver un objeto que implemente la interfaz StreamResponse en el manejador de evento («event handler») que procesa la petición.

Aquí el código de parte de un manejador de evento que construye y devuelve un objeto que implementa la interfaz StreamResponse:

...
@Inject
private ResponseCompressionAnalyzer responseCompressionAnalyzer;
...
StreamResponse onSuccessFromForm() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream os = baos;
String contentEncoding = null;
if (responseCompressionAnalyzer.isGZipSupported()) {
os = new GZIPOutputStream(os, 128 * 1024);
contentEncoding = "gzip";
}
...
os.write(...);
...
return new InputStreamResponse(nombre, contentEncoding, "application/octet-stream", new ByteArrayInputStream(os.toByteArray()));
}
...
view raw Page.java hosted with ❤ by GitHub
Una posible implementación de la clase StreamResponse podría ser la siguiente que podríamos reutilizar siempre que tengamos que devolver un archivo:

package com.appspot.mukomix.tapestry.misc;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import org.apache.commons.lang3.StringUtils;
import org.apache.tapestry5.StreamResponse;
import org.apache.tapestry5.services.Response;
public class InputStreamResponse implements StreamResponse {
private String nombre;
private String contentEncoding;
private String contentType;
private InputStream stream;
public InputStreamResponse(String nombre, String contentEncoding, String contentType, InputStream stream) {
this.nombre = nombre;
this.contentEncoding = contentEncoding;
this.contentType = contentType;
this.stream = stream;
}
public String getContentEncoding() {
return contentType;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public InputStream getStream() throws IOException {
return stream;
}
@Override
public void prepareResponse(Response response) {
if (StringUtils.isNotBlank(contentEncoding)) {
response.setHeader("Content-Encoding", contentEncoding);
}
response.setHeader("Content-Disposition", MessageFormat.format("attachment; filename=\"{0}\"", nombre));
}
}
Si estuviésemos devolviendo un archivo de texto y si el cliente acepta compresión gzip según las cabeceras que nos ha enviado en la petición (Accept-encoding: gzip) podríamos devolver el resultado comprimido con este formato ahorrado ancho de banda y muy posiblemente reduciendo el tiempo de descarga del archivo. Haciendo uso del servicio ResponseCompressionAnalyzer y del método isGZipSupported podemos averiguarlo de forma muy sencilla.

En la siguiente entrada de documentación sobre Apache Tapestry puedes encontrar más entradas, ejemplos y documentación.

Para finalizar, decir que la forma indicada en este ejemplo para convertir el OutputStream al InputStream que necesita el objeto StreamResponse puede ser mejorado con el uso de un PipedInputStream. La manera indicada en el ejemplo con el ByteArrayOuputStream obliga a mantener en memoria el contenido completo generado antes de ser enviado al cliente además de no empezar a enviarlo hasta que se genera por completo, si se trata de unos pocos MiB puede ser suficiente pero si son varias decenas la aplicación puede presentar algún problema en el uso de memoria.

Referencia:
http://tapestry.apache.org
http://betterexplained.com/articles/how-to-optimize-your-site-with-gzip-compression/