Catálogo de Patrones de Diseño J2EE. I.- Capa de Presentación

El mecanismo de manejo de peticiones de la capa de presentación recibe muchos tipos diferentes de peticiones, cada uno de los cuales requiere varios tipos de procesamiento. Algunas peticiones simplemente requieren su reenvio al componente manejador apropiado, mientras que otras peticiones deben ser modificadas, auditadas, o descomprimidas antes de su procesamiento posterior.

. Problema:

Se requiere un pre-procesamiento y un post-procesamiento de unas peticiones o respuestas de un cliente Web.

Cuando una petición entra a una aplicación Web, normalmente debe pasar varios test de entrada antes del estado de procesamiento principal. Por ejemplo,

  • ¿Se ha autentificado el cliente?
  • ¿Tiene el cliente una sesión válida?
  • ¿La dirección IP del cliente es de una red conocida?
  • ¿Viola alguna restricción el path de la petición?
  • ¿Qué codificación usa el cliente para enviar los datos?
  • ¿Soportamos el tipo de navegador del cliente?

Algunos de estos chequeos son tests, que resultan en una respuesta de si o no que determina si continuará el procesamiento. Otros chequeos manipulan el stream de datos entrantes a una forma aceptable para el procesamiento.

La solución básica consiste en un serie de chequeos condicionales, si cualquiera de ellos falla la petición se aborta. Las sentencias if/else anidadas son una estrategia estándar, pero esta solución tiene fragilidad de código y un estilo de programación de copiar-y-pegar, porque el flujo del filtrado y la acción de los filtros se compila dentro de la aplicación.

La clave para solventar este problema de una forma flexible y no obstrusiva es tener un mecanismo simple para añadir y eliminar componentes de procesamiento, en el que cada componente completa una acción de filtrado específica.

. Causas

  • Procesamiento común, como un chequeo del esquema de codificación de datos o la información de login de cada petición, completo por cada petición.
  • Se desea la centralización de la lógica común.
  • Se debería facilitar la adición o eliminación de sevicios sin afectar a los componentes existentes, para que se puedan utilizar en gran variedad de combinaciones, como
    • Logging y autentificación.
    • Depuración y transformación de la salida para un cliente específico
    • Descomprensión y conversión del esquema de codificación de la entrada.

. Solución

Crear filtros conectables para procesar servicios comunes de una forma estándar sin requerir cambios en el código principal del procesamiento de la petición. Los filtros interceptan las peticiones entrantes y las respuestas salientes, permitiendo un pre y post-procesamiento. Podemos añadir y eliminar estos filtros a discrección, sin necesitar cambios en nuestro código existente.

Podemos, en efecto, decorar nuestro procesamiento principal con una veriedad de servicios comunes, como la seguridad, el logging, el depurado, etc. Estos filtros son componentes independientes del código de la aplicación principal, y pueden añadirse o eliminarse de forma declarativa. Por ejemplo, se podría modificar un fichero de configuración de despliegue para configurar una cadena de filtros. Cuando un cliente pide un recurso que corresponde con este mapeo de URL configurado, se procesa cada filtro de la cadena antes de poder invocar el recurso objetivo.

. Estructura

La siguiente figura representa el diagrama de clases del patrón Intercepting Filter.

. Participantes y Responsabilidades

La siguiente figura representa el diagrama de la secuencia del patrón Intercepting Filter.

. FilterManager

El FilterManager maneja el procesamiento de filtros. Crea el FilterChain con los filtros apropiados, en el orden correcto e inicia el procesamiento.

. FilterChain

El FilterChain es una collection ordenada de filtros indenpendientes.

. FilterOne, FilterTwo, FilterThree

Estos son los filtros individuales que son mapeados a un objetivo. El FilterChain coordina su procesamiento.

. Target

El Target es el recurso que el cliente ha solicitado.

. Estrategias

. Custom Filter

El filtro se implementa mediante una estrategia personalizada definida por el desarrollador. Esto es menos flexible y menos poderoso que la preferida Estrategia de Filtro Estándar que veremos en la siguiente sección y que sólo está disponible en contenedores que soporten la especificación servlet 2.3. La estrategia de filtro personalizado es menos poderosa porque no puede proporcionar una envoltura para los objetos request y response de una forma estándar y portable. Además, el objeto request no se puede modificar, y se debe introducir alguna suerte de mecanismo de buffer si los filtros son para controlar los streams de salida. Para implementar esta estrategia, el desarrollador podría utilizar el patrón Decorator [GoF] para envolver los filtros alrededor de la lógica principal del procesamiento de la petición. Por ejemplo, podría haber un filtro de depuración que envuelva un filtro de autentificación. Los siguientes fragmentos de código muestran como se podrían crear estos mecanismos de forma programátia:

DebuggingFilter:

public class DebuggingFilter implements Processor {
  private Processor target;

  public DebuggingFilter(Processor myTarget) {
    target = myTarget;
  }

  public void execute(ServletRequest req, 
  ServletResponse res) throws IOException, 
    ServletException    {
    //Do some filter processing here, such as 
    // displaying request parameters
    target.execute(req, res);
  }
}

CoreProcessor:

 
public class CoreProcessor implements Processor {
  private Processor target;
  public CoreProcessor()   {
    this(null);
  }

  public CoreProcessor(Processor myTarget)   {
    target = myTarget;
  }

  public void execute(ServletRequest req, 
      ServletResponse res) throws IOException, 
      ServletException   {
    //Do core processing here
  }
}

En el controlador servlet, hemos delegado en un método llamado processRequest para manejar las peticiones entrantes:

 
public void processRequest(ServletRequest req, 
  ServletResponse res) 
  throws IOException, ServletException {
  Processor processors = new DebuggingFilter( 
    new AuthenticationFilter(new CoreProcessor()));
  processors.execute(req, res);

  //Then dispatch to next resource, which is probably 
  // the View to display
  dispatcher.dispatch(req, res);
}

Sólo para propósitos de ejemplo, imagina que cada componente de procesamiento escribe en la salida estándar cuando se ejecuta. El siguiente ejemplo muestra la posible salida de la ejecución:

Debugging filter preprocessing completed...
Authentication filter processing completed...
Core processing completed...
Debugging filter post-processing completed...

Se ejecuta una cadena de procesadores en orden. Cada procesador, excepto el último de la cadena, se considera un filtro. En el componente procesador final es donde se encapsula el procesamiento principal que queremos completar para cada petición. Con este diseño, necesitaremos cambiar el código de la clase CoreProcessor, así como cualquier clase de filtro, cuando querramos modificar la forma de manejar las peticiones.

La siguiente figura muestra un diagrama de secuencia que describe el flujo de control cuando se utiliza el código de filtro de los ejemplos anteriores:

Observa que cuando usamos una implementación de Decorator, cada filtro invoca directamente al siguiente filtro, aunque usando un interface genérico. De forma alternativa, esta estrategia se puede implementar utilizando un FilterManager y un FilterChain. En este caso, estos componentes manejan el procesamiento de los filtros y los filtros individuales no se comunican con ningún otro filtro directamente. Este diseño se aproxima al de una implementación compatible con Servlet 2.3, aunque aún así es una estrategia personalizada. En el primero de los siguientes listado veremos la clase FilterManager que crea un objeto FilterChain.

public class FilterManager {
  public void processFilter(Filter target, 
    javax.servlet.http.HttpServletRequest request, 
    javax.servlet.http.HttpServletResponse response) 
    throws javax.servlet.ServletException, 
      java.io.IOException       {                    
    FilterChain filterChain = new FilterChain();

    // The filter manager builds the filter chain here 
    // if necessary

    // Pipe request through Filter Chain
    filterChain.processFilter(request, response);

    //process target resource
    target.execute(request, response);
  }
}

El FilterChain añade filtros a la cadena en el orden apropiado (por brevedad lo hemos hecho en el constructor, pero normalmente se haría en el lugar del comentario), procesa los filtros, y finalmente procesa el recurso objetivo:

public class FilterChain {
  // filter chain 
  private Vector myFilters = new Vector();

  // Creates new FilterChain 
  public FilterChain()  {
    // plug-in default filter services for example 
    // only. This would typically be done in the 
    // FilterManager, but is done here for example 
    // purposes
    addFilter(new DebugFilter());
    addFilter(new LoginFilter());
    addFilter(new AuditFilter());
  }

  public void processFilter( 
    javax.servlet.http.HttpServletRequest request,
    javax.servlet.http.HttpServletResponse response)
  throws javax.servlet.ServletException, 
    java.io.IOException         {
    Filter filter;

    // apply filters
    Iterator filters = myFilters.iterator();
    while (filters.hasNext())
    {
      filter = (Filter)filters.next();
      // pass request & response through various 
      // filters
      filter.execute(request, response);
    }
  }

  public void addFilter(Filter filter)  {
    myFilters.add(filter);
  }
}

En la siguiente figura podemos ver el diagrama de secuencia de este código:

Esta estrategia no nos permite crear filtros que sean tan flexibles y poderosos como nos gustaría. Por una cosa, los filtros se añade y eliminan programáticamente. Aunque podríamos escribir un mecanismo propietario para manejar la adición y eliminación de filtros mediante un fichero de configuración, aún no tendríamos ninguna forma de envolver los objetos request y response. Además, sin un mecanismo de buffer sofisticado, esta estrategia no proporcionará post-procesamiento flexible.

La Estrategia de Filtro Estándar proporciona soluciones para estos problemas, utilizando características de la especificación Servlet 2.3, que proporciona una solución estándar al dilema de los filtros.

. Standard Filter

Los filtros se controlan de forma declarativa utilizando un descriptor de despliegue, según se describe en la especificación Servlet 2.3. Esta especificación incluye un mecanismo estándar para construir cadenas de filtos y poder añadir y eliminar filtros de esa cadena de forma transparente. Los filtros se contruyen sobre interfaces, y se añaden o eliminan de una forma declarativa modificando el descriptor de despliegue de una aplicación Web.

Nuestro ejemplo para esta estrategia será crear un filtro que preprocese peticiones de cualquier tipo de codificación como las que podríamos manejar en el código principal de manejo de peticiones. ¿Por qué podría ser esto necesario? Los formularios HTML que incluyen un upload de ficheros utilizan un tipo de codificación diferente a la mayoría de formularios. Así, los datos del formulario que acompañan al upload no están disponibles mediante simples llamadas a getParameter(). Por eso, creamos dos filtros que preprocesen las peticiones, traduciendo todos los tipos de codificación en un sólo formato consistente. El formato que elegimos es hacer que todos los datos del formulario estén disponibles como atributos de la petición.

Un fitlro maneja la forma de codificación estándar para el tipo application/x-www-form-urlencoded y el otro maneja el tipo de codificación menos común multipart/form-data, que se utiliza en los formularios que incluyen uploads. Los filtros traducen todos los datos del formulario en atributos de la petición, para que el mecanismo principal de manejo de peticiones pueda trabajar con todas las peticiones de la misma manera. en lugar de hacerlo con los casos especiales de las diferentes codificaciones.

El siguiente ejemplo muestra un filtro que traduce las peticiones utilizando el esquema de codificación de formularios estándar:

public class StandardEncodeFilter 
  extends BaseEncodeFilter {
  // Creates new StandardEncodeFilter
  public StandardEncodeFilter()   {  }

  public void doFilter(javax.servlet.ServletRequest 
    servletRequest,javax.servlet.ServletResponse 
    servletResponse,javax.servlet.FilterChain 
    filterChain) 
  throws java.io.IOException, 
    javax.servlet.ServletException {

    String contentType = 
      servletRequest.getContentType();
    if ((contentType == null) || 
      contentType.equalsIgnoreCase(
        "application/x-www-form-urlencoded"))     {
      translateParamsToAttributes(servletRequest, 
        servletResponse);
    }

    filterChain.doFilter(servletRequest, 
      servletResponse);
  }

  private void translateParamsToAttributes(
    ServletRequest request, ServletResponse response)
  {
    Enumeration paramNames = 
        request.getParameterNames();

    while (paramNames.hasMoreElements())     {
      String paramName = (String) 
          paramNames.nextElement();

      String [] values;

      values = request.getParameterValues(paramName);
      System.err.println("paramName = " + paramName);
      if (values.length == 1)
        request.setAttribute(paramName, values[0]);
      else
        request.setAttribute(paramName, values);
    }
 }
}

El ejemplo siguiente muestra el filtro que maneja las traduciones de las peticiones que utilizan el esquema de codificación multipart.:

public class MultipartEncodeFilter extends 
  BaseEncodeFilter {
  public MultipartEncodeFilter() { }
  public void doFilter(javax.servlet.ServletRequest 
    servletRequest, javax.servlet.ServletResponse 
    servletResponse,javax.servlet.FilterChain 
    filterChain)
  throws java.io.IOException, 
    javax.servlet.ServletException {
    String contentType = 
      servletRequest.getContentType();   
    // Only filter this request if it is multipart 
    // encoding                
    if (contentType.startsWith(
                "multipart/form-data")){
      try {
        String uploadFolder = 
          getFilterConfig().getInitParameter(
              "UploadFolder");
        if (uploadFolder == null) uploadFolder = ".";

        /** The MultipartRequest class is: 
        * Copyright (C) 2001 by Jason Hunter 
        * <[email protected]>. All rights reserved. 
        **/
        MultipartRequest multi = new 
          MultipartRequest(servletRequest, 
                           uploadFolder,
                           1 * 1024 * 1024 );
        Enumeration params = 
                 multi.getParameterNames();
        while (params.hasMoreElements()) {
          String name = (String)params.nextElement();
          String value = multi.getParameter(name);
          servletRequest.setAttribute(name, value);
        }

        Enumeration files = multi.getFileNames();
        while (files.hasMoreElements()) {
          String name = (String)files.nextElement();
          String filename = multi.getFilesystemName(name);
          String type = multi.getContentType(name);
          File f = multi.getFile(name);
          // At this point, do something with the 
          // file, as necessary
        }
      }
      catch (IOException e)
      {
        LogManager.logMessage(
          "error reading or saving file"+ e);
      }
    } // end if
    filterChain.doFilter(servletRequest, 
                         servletResponse);
  } // end method doFilter()
}

El código de estos filtros se basa en la especificación servlet 2.3. También se usa un filtro base, desde el que descienden estos dos filtros. El filtro base mostrado en el siguiente ejemplo proporciona el comportamiento por defecto para los métodos de retrollamada del filtro estándar:

public class BaseEncodeFilter implements 
      javax.servlet.Filter {
  private javax.servlet.FilterConfig myFilterConfig;

  public BaseEncodeFilter()     {  }

  public void doFilter(
    javax.servlet.ServletRequest servletRequest, 
    javax.servlet.ServletResponse servletResponse,
    javax.servlet.FilterChain filterChain) 
  throws java.io.IOException,
    javax.servlet.ServletException {
    filterChain.doFilter(servletRequest, 
        servletResponse);
  }

  public javax.servlet.FilterConfig getFilterConfig() 
  {
    return myFilterConfig; 
  }
    
  public void setFilterConfig(
    javax.servlet.FilterConfig filterConfig) {
      myFilterConfig = filterConfig;
  }
}

Abajo tenemos un extracto del descriptor de despliegue de la aplicación Web que contiene este ejemplo. Muestra cómo se registran estos dos filtros y luego los mapea a un recurso, en este caso un sencillo servlet de prueba.

.
.
.
<filter>
    <filter-name>StandardEncodeFilter</filter-name>
    <display-name>StandardEncodeFilter</display-name>
    <description></description>
    <filter-class> corepatterns.filters.encodefilter.
            StandardEncodeFilter</filter-class>
  </filter>
  <filter>
    <filter-name>MultipartEncodeFilter</filter-name>
    <display-name>MultipartEncodeFilter</display-name>
    <description></description>
    <filter-class>corepatterns.filters.encodefilter.
            MultipartEncodeFilter</filter-class>
    <init-param>
      <param-name>UploadFolder</param-name>
      <param-value>/home/files</param-value>
    </init-param>
 </filter>
.
.
.
<filter-mapping>
    <filter-name>StandardEncodeFilter</filter-name>
    <url-pattern>/EncodeTestServlet</url-pattern>
  </filter-mapping>
  <filter-mapping>
    <filter-name>MultipartEncodeFilter</filter-name>
    <url-pattern>/EncodeTestServlet</url-pattern>
  </filter-mapping>
.
.
.

En la siguiente figura podemos ver el diagrama de secuencia de este ejemplo:

Los filtros StandardEncodeFilter y MultiPartEncodeFilter interceptan el control cuando un cliente hace una petición al servlet controlador. El contenedor acepta el rol de manejador de filtros y conduce el control a estos filtros llamando a sus métodos doFilter. Después de completar su procesamiento, cada filtro pasa el control al FilterChain que lo contiene, que está instruido para ejecutar el siguiente filtro. Una vez que el control ha pasado por los dos filtros, el siguiente componente en recibir el control es el recurso objetivo real, en este caso el servlet controlador.

Los filtros, según lo soporta la especificación Servlet 2.3, también soportan la envoltura de los objetos request y response. Esta característica proporciona un mecanismo mucho más podereos que el que se puede construir utilizando la implementación personlizada de la sección anterior. Por supuesto, también podríamos construir una aproximación híbrida combinando las dos estrategias.

. Base Filter

Un filtro base sirve como una superclase común para todos los filtros. Las características comunes se pueden encapsular en el filtro base y se pueden compartir entre todos los filtros. Por ejemplo, un filtro base es un buen lugar para incluir el comportamiento por defecto de los métodos de retrollamada del contenedor, como hemos visto en la sección anterior. El siguiente fragmento de código nos muestra cómo hacer esto:

public class BaseEncodeFilter implements 
  javax.servlet.Filter {
  private javax.servlet.FilterConfig myFilterConfig;
        
  public BaseEncodeFilter()             {       }

  public void doFilter(javax.servlet.ServletRequest     
    servletRequest,javax.servlet.ServletResponse 
    servletResponse, javax.servlet.FilterChain 
    filterChain) throws java.io.IOException, 
    javax.servlet.ServletException {

    filterChain.doFilter(servletRequest, 
      servletResponse);
  }

  public javax.servlet.FilterConfig getFilterConfig() {
    return myFilterConfig; 
  }
    
  public void 
  setFilterConfig(javax.servlet.FilterConfig 
    filterConfig) {
    myFilterConfig = filterConfig;
  }
}
. Template Filter

Usar un filtro base del que descienden todos los demás permite a la clase base proporcionar la funcionalidad de Plantilla de Métodos [GoF]. En este caso, el filtro base se utiliza para dictar los pasos generales que debe completar cada filtro, aunque deja las especifidades de cómo completar esos pasos en la subclase de cada filtro. Normalmente, esto se definiría de forma burda, métodos básicos que simplemente imponen una estructura limitada a cada plantilla. Esta estrategia también se puede combinar con cualquier otra estrategia de filtros. Los siguientes listados muestran como utilizar esta estrategia con la Estrategia de Filtro Declarado:

public abstract class TemplateFilter implements 
  javax.servlet.Filter {
  private FilterConfig filterConfig;

  public void setFilterConfig(FilterConfig fc) { 
    filterConfig=fc; 
  }

  public FilterConfig getFilterConfig()         { 
    return filterConfig; 
  }

  public void doFilter(ServletRequest request, 
    ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    // Common processing for all filters can go here 
    doPreProcessing(request, response, chain);

    // Common processing for all filters can go here
    doMainProcessing(request, response, chain);

    // Common processing for all filters can go here 
    doPostProcessing(request, response, chain);

    // Common processing for all filters can go here

    // Pass control to the next filter in the chain or 
    // to the target resource
    chain.doFilter(request, response);
  }
  public void doPreProcessing(ServletRequest request,   
    ServletResponse response, FilterChain chain) {
  }

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

  public abstract void doMainProcessing(ServletRequest 
   request, ServletResponse response, FilterChain 
   chain);
}

Dando esta definición de clase para TemplateFilter, cada filtro se implementa como una subclase que sólo debe implementar el método doMainProcessing. Estas subclases tienen la opción de implementar los otros tres métodos si lo desean. Abajo tenemos un ejemplo de una subclase que implementa el método obligatorio (dictado por nuestra plantilla de filtro) y el método de preprocesamiento opcional.

public class DebuggingFilter extends TemplateFilter {
  public void doPreProcessing(ServletRequest req, 
    ServletResponse res, FilterChain chain) {
    //do some preprocessing here
  }

  public void doMainProcessing(ServletRequest req, 
    ServletResponse res, FilterChain chain) {
    //do the main processing;
  }
}

En la siguiente figura podemos ver el diagrama de secuencia de esta estrategia:

En el diagrama de secuencia, las subclases, como DebuggingFilter, definen el procesamiento sobreescribiendo el método abstracto doMainProcessing, y, opcionalmente, doPreProcessing y doPostProcessing. Así, la plantilla de filtro impone una estructura para el procesamiento de todos los filtros, así como proporciona un lugar para encapsular el código que es común para cada filtro.

. Consecuencias

  • Centraliza el Control con Controladores de Acoplamiento Ligero
    Los filtros proporcionan un lugar centralaizado para controlar el procesamiento a través de múltiples peticiones, como lo hace el controlador. Los filtros están mejor preparados para manejar las peticiones y respuestas para el control último por un recurso objetivo, como un controlador. Además, un controlador frecuentemente junta el control de numerosos sevicios comunes y no relacionados, como la autentificación, el login, la encriptación, etc., mientras que los filtros nos permiten controladores de acoplamiento más ligero, que se pueden combinar.
  • Mejora la Reutilización
    Los filtros promueven la limpieza del particionamiento de la aplicación y aconsejan su reutilización. Estos interceptores conectables se añaden y eliminan al código existente de forma transparente y debido a su interface estándar, funcionan en cualquier combinación y son reutilizables por varias presentaciones.
  • Configuración Declarativa y Flexible
    Se pueden combinar numerosos servicios en varias permutaciones sin tener que recompilar ni una sola vez el código fuente.
  • La Compartición Información es Ineficiente
    Compartir información entre filtros puede ser ineficiente, porque por definición todo filtro tiene acoplamiento ligero. Si se deben compartir grandes cantidades de información entre los filtros, esta aproximación podría ser muy costosa.

. Patrones Relacionados

  • Front Controller
    El controlador resuelve algunos problemas similares, pero está mejor diseñado para manejar el procesamiento principal.
  • Decorator [GoF]
    El patrón Intercepting Filter está relacionado con el patrón Decorator, que proporciona envolturas conectables dinámicamente.
  • Template Method [GoF]
    El patrón de Plantillas de Métodos se utiliza para implementar la Estrategia de Plantilla de Filtros.
  • Interceptor [POSA2]
    El patrón Intercepting Filter está relacionado con el patron Interceptor, que permite que se pueden añadir servicios de forma transparente y dispararlos automáticamente.
  • Pipes and Filters [POSA1]
    El patrón Intercepting Filter está relacionado con el patrón Pipes and Filters.

COMPARTE ESTE ARTÍCULO

ENVIAR A UN AMIGO
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN GOOGLE +
SIGUIENTE ARTÍCULO

¡SÉ EL PRIMERO EN COMENTAR!
Conéctate o Regístrate para dejar tu comentario.