5.8 Wrappers

Hasta ahora hemos visto como interceptar la petición que se realiza a un determinado recurso de nuestra web mediante filtros, pero, ¿y si queremos interceptar la respuesta que nos devuelve el servidor para analizarla o modificarla?

Cuando desde nuestro filtro pasemos el procesamiento de la petición al siguiente elemento de la cadena (doFilter), delegaremos en este siguiente elemento el procesamiento de la petición y la generación de la respuesta. Supongamos que este elemento es el recurso final que se había solicitado. En este caso el contenido de este recurso será escrito en el objeto respuesta, lo cual producirá que dicho contenido sea devuelto al cliente.

Sin embargo, nosotros no queremos que sea devuelto directamente al cliente, sino que queremos procesarla previamente en nuestro filtro antes de devolverla. Con este objeto ServletResponse (HttpServletResponse) no podremos hacer esto, ya que cuando se escribe en él lo que se hace es devolver la respuesta al cliente, y una vez escrita no podemos acceder nuevamente a ella ni modificarla.

La solución a nuestro problema es sustituir el objeto respuesta que proporcionamos al siguiente elemento de la cadena por un objeto de respuesta creado por nosotros.

5.8.1 ¿Qué es un wrapper?

Un wrapper es un objeto que envuelve al objeto original, de forma que no se acceda directamente al objeto original sino al wrapper. El wrapper implementará la misma interfaz del objeto al que envuelve, de forma que externamente se trabajará con él de la misma forma, por lo que podemos sustituir el original por el wrapper siendo esto transparente a los sucesivos elementos que vayan a manipular este objeto.

Cuando se llame a un método del wrapper podrá, o bien redirigir la llamada al correspondiente método del objeto original al que envuelve, o bien tratar por si mismo la llamada a dicho método. De esta forma, podremos redefinir el comportamiento que tendrán determinadas operaciones.

Encontramos para nuestro fin wrappers para la petición y la respuesta: ServletRequestWrapper (HttpServletRequestWrapper) y ServletResponseWrapper (HttpServletResponseWrapper). Con ellos podremos crear implementaciones propias del objeto petición y respuesta que envuelvan a los originales, pudiendo de esta forma redefinir el comportamiento de determinadas operaciones.

Nos centraremos en el wrapper de la respuesta. Con él podemos evitar que la respuesta se envie directamente al cliente. En lugar de esto, cuando se escriba la salida en este objeto wrapper de la respuesta podemos hacer que guarde dicha salida en un buffer interno. Una vez procesados todos los elementos de la cadena que están después de nuestro filtro (tras llamar a doFilter), se habrá escrito la salida generada en el buffer del wrapper. En este momento podemos analizar esta salida, modificarla si es necesario, y enviarla a través del objeto respuesta original.

5.8.2 Implementación de un wrapper

Para implementar un wrapper deberemos crearnos una subclase de la clase del wrapper adecuado para nuestro caso (petición o respuesta), y redefinir en esta subclase las operaciones cuyo comportamiento queramos cambiar. El funcionamiento por defecto de las operaciones que no redefinamos será redirigir la petición al método correspondiente del objeto (petición o respuesta) original.

Vamos a ver esto con un ejemplo de implementación de un wrapper de la respuesta que guarda en un buffer la respuesta generada por el servidor, para poder ser procesada por nuestro filtro.

Puesto que queremos envolver la respuesta, tendremos que crearnos una subclase de ServletResponseWrapper:

public class GenericResponseWrapper extends HttpServletResponseWrapper {

Dentro de esta clase deberemos tener el buffer donde vayamos a escribir la salida. Dado que en la salida se puede escribir tanto como flujo de bytes como de caractéres, para que sea más genérico convendrá crear el buffer como array de bytes, de forma que se pueda escribir en él de las dos formas:

	private ByteArrayOutputStream output;
En el constructor de la clase simplemente deberemos proporcionar el objeto respuesta original (al cual estaremos envolviendo). Lo que hacemos aquí es utilizar el constructor de la superclase proporcionándole la respuesta original, de forma que se encargue de redirigir a él las operaciones predeterminadas. Además deberemos crear nuestro buffer de bytes donde se escribirá la respuesta:
	public GenericResponseWrapper(HttpServletResponse response) { 
super(response);
output = new ByteArrayOutputStream();
}
Proporcionaremos además un método para obtener los datos escritos en el buffer:
	public byte[] getData() { 
return output.toByteArray();
}

Cuando alguien quiera devolver una respuesta al cliente lo que hará será obtener el flujo de salida del objeto respuesta y escribir en él. Por defecto este flujo envia los datos al cliente. Sin embargo podemos evitar que esto ocurra haciendo que los flujos que devuelva sirvan para escribir en el buffer, y no para enviar la respuesta al cliente. Se puede enviar la respuesta de dos formas: mediante un flujo de bytes (getOutputStream), o mediante un flujo de carácteres (getWriter), por lo que deberemos redefinir ambos métodos.

	public ServletOutputStream getOutputStream() { 
return new FilterServletOutputStream(output);
}

public PrintWriter getWriter() {
return new PrintWriter(getOutputStream(), true);
}
}

En el caso del flujo de bytes, deberemos devolverlo como un ServletOutputStream. Por lo tanto tendremos que crearnos un tipo propio de ServletOutputStream que escriba en nuestro buffer:

public class FilterServletOutputStream extends ServletOutputStream { 
private DataOutputStream stream;


public FilterServletOutputStream(OutputStream output) {
stream = new DataOutputStream(output);
}


public void write(int b) throws IOException {
stream.write(b);
}


public void write(byte[] b) throws IOException {
stream.write(b);
}


public void write(byte[] b, int off, int len) throws IOException {
stream.write(b, off, len);
}
}

Este será el flujo que utilicemos para escribir la respuesta en forma de bytes en nuestro buffer interno.

Aunque a primera vista parezca compleja la creación de dicho wrapper, tiene la ventaja de ser reutilizable para cualquier aplicación en la que necesitemos interceptar la respuesta generada por el servidor.

5.8.3 Utilización de un wrapper

Para utilizar el wrapper que hemos creado, deberemos instanciarlo a partir del objeto de respuesta original que le ha sido proporcionado a nuestro filtro. Esto lo haremos antes de que se haya generado el contenido del recurso solicitado, es decir, antes de llamar a doFilter.

public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
{

...

GenericResponseWrapper wrapper = new GenericReponseWrapper(response);

Una vez hemos creado nuestro propio objeto respuesta que envuelve a la respuesta original, podemos utilizarlo para que el servidor escriba el contenido del recurso solicitado en él. Para esto realizaremos la llamada a doFilter proporcionando como respuesta este wrapper que hemos creado:

	chain.doFilter(request, wrapper);

Una vez ejecutado este método se habrá generado la respuesta en el objeto de respuesta proporcionado, en este caso habrá sido en nuestro wrapper. Por lo tanto podemos obtener y procesar la respuesta según la función de nuestro filtro:

	byte [] datos = wrapper.getData();


... // Procesar datos segun la funcion del filtro

Por último, para que el cliente pueda ver esta respuesta, deberemos escribirla en el objeto respuesta original:

	OutputStream out = response.getOutputStream();
out.write(datos);
out.close();

}

Con esto vemos que habremos podido procesar la salida generada en nuestro filtro, y enviarla al cliente para que pueda ser visualizada correctamente.

5.9 Ejemplos de filtros

Vamos a ver a continuación una serie de ejemplos de usos comunes de los filtros, y cómo implementaríamos dichos filtros, utilizando distintos elementos que hemos visto durante el curso.

5.9.1 Acceso restringido

Una primera aplicación sencilla de los filtros es prohibir el acceso a cierta parte de nuestra web. Cuando un usuario intente acceder a dicha parte, se comprobará si este usuario está registrado. Si lo está se le dejará pasar normalmente, pero si no se prohibirá el acceso, redireccionando a la página de login de usuarios.

public class RestringirAcceso implements Filter {

Cuando se invoca el filtro querrá decir que un usuario intenta acceder a la zona restringida.

    public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
{
// Se intenta acceder a la zona restringida

Comprobamos si el usuario está registrado en el servidor. Para ello utilizamos la información de sesión, donde almacenaremos el login del usuario en caso de estar registrado.

        // Solo podemos comprobar la sesión en el caso de tener una petición HTTP
if(request instanceof HttpServletRequest &&
response instanceof HttpServletResponse)
{

HttpServletRequest http_request = (HttpServletRequest)request;
HttpServletResponse http_response = (HttpServletResponse)response;

// *********** Comprobamos si el usuario se ha registrado ***********
// En nuestra aplicación si el usuario se ha registrado habremos establecido
// el atributo usuario de la sesion al nombre del usuario, si no será null.

if(http_request.getSession().getAttribute("usuario") != null)
 

Si hay un login almacenado, procesamos la petición de forma normal.

            {
// Continuamos de forma normal con la petición
chain.doFilter(request, response);
}

Si no, redireccionamos a la página de login, para que el usuario se registre.

            else 
{
// Redireccionamos a la página de login
response.sendRedirect("/ejemplo/login.jsp");
}

} else {
// Si no es una petición HTTP simplemente procesamos la petición
chain.doFilter(request, response);
}
}
}

5.9.2 Ranking de páginas más visitadas

Otra posible aplicación es registrar el número de visitas que se hacen a cada página, de forma que podremos obtener un listado de las páginas favoritas de los usuarios dentro de nuestro sitio web. Para ello instalaremos un filtro que intercepte las peticiones a cualquier página. Cada vez que el filtro se invoque, querrá decir que se ha visitado una página. Lo que deberemos hacer en este momento es:

Determinar la dirección de la página que se ha solicitado

public class Ranking implements Filter {

// Objeto que encapsula la conexión a la BD de páginas
BDPaginas bdPaginas = null;

public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
{
// Solo podemos ver el recurso solititado en el caso de tener una petición HTTP
if(request instanceof HttpServletRequest)
{

HttpServletRequest http_request = (HttpServletRequest)request;

// Miramos que recurso está siendo solicitado
String uri = http_request.getRequestURI();

Tendremos una base de datos con una entrada para cada página, donde se contabilizan el número de visitas. Si no existe entrada para la página visitada, la crearemos con una visita.

            if(bdPaginas.existePagina(uri)
{
// La página ya esta registrada en la BD
// y solo tenemos que incrementar su contador
bdPaginas.incrementaContador(uri);
}

Si ya existe entrada para esta página en la BD, incrementaremos el número de visitas.

            else 
{
// La página se está visitando por primera vez
// Debemos registrarla en la BD con contador a 1 (1 visita)
bdPaginas.insertaPagina(uri);
}
}

Procesamos la petición de forma normal.

        chain.doFilter(request, response);
}

En los métodos init y destroy abriremos y cerraremos respectivamente la conexión con nuestra base de datos. De este forma evitamos tener que estar abriendo y cerrando una conexión para cada petición.

    public void init(FilterConfig config) {
bdPaginas = new BDPaginas();
bdPaginas.conectar();
}

public void destroy() {
bdPaginas.cerrar();
}
}

5.9.3 Extracción automática de información

Imaginemos que en el ranking queremos, además de la dirección, registrar el título de la página. A partir de la información de la petición y la respuesta ordinaria no podemos obtener dicha información, ya que se refiere al contenido de la página. Para ello tendremos que utilizar un wrapper, que obtenga la respuesta generada por el servidor, de manera que podamos analizarla y extraer de ella el título de la página.

public class RankingTitulo implements Filter {

// Objeto que encapsula la conexión a la BD de páginas
BDPaginas bdPaginas = null;

public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
{
// Solo podemos ver el recurso solititado en el caso de tener una petición HTTP
if(request instanceof HttpServletRequest &&
response instanceof HttpServletResponse)
{

HttpServletRequest http_request = (HttpServletRequest)request;
HttpServletResponse http_response = (HttpServletResponse)response;

// Miramos que recurso está siendo solicitado
String uri = http_request.getRequestURI();

if(bdPaginas.existePagina(uri)
{
// La página ya esta registrada en la BD
// y solo tenemos que incrementar su contador
bdPaginas.incrementaContador(uri);
}

Cuando se visite una página por primera vez, para registrarla en la base de datos tendremos que obtener la información del título. Creamos un wrapper, y procesamos la petición utilizando dicho wrapper como objeto respuesta.

            else 
{
// La página se está visitando por primera vez
// Debemos obtener su titulo para registrarla en la BD

// Envolvemos la respuesta con nuestro wrapper generico
GenericResponseWrapper wrapper = new GenericResponseWrapper(http_response);

// Procesamos la petición
chain.doFilter(http_request, wrapper);

Una vez hecho esto, tendremos en el wrapper el contenido de la página generado. Podemos obtenerlo y analizarlo, buscando en él la etiqueta <title>.

                // En este momento ya diponemos de la respuesta en el wrapper
// La analizamos para obtener el valor de su etiqueta <title>
byte [] datos = wrapper.getData();
HtmlParser parser = new HtmlParser(datos);
String titulo = parser.getTitle();

Una vez obtenido el título, podremos registrar en la base de datos la entrada de la página.

                // Ahora podemos registrar ya la página con sus datos
bdPaginas.insertaPagina(uri, titulo);

Por último, tendremos que hacer que la respuesta del wrapper pase al cliente, enviándola al objeto respuesta original.


// Por último, debemos devolver la respuesta al cliente de forma
// que pueda visualizar el recurso solicitado
OutputStream out = response.getOutputStream();
out.write(datos);
out.close();
}
} else {
// Si no es HTTP procesamos la petición de forma ordinaria
chain.doFilter(request, response);
}
}

public void init(FilterConfig config) {
bdPaginas = new BDPaginas();
bdPaginas.conectar();
}

public void destroy() {
bdPaginas.cerrar();
}
}