viernes, 13 de julio de 2012

Extender las validaciones de Apache Tapestry

Apache Tapestry
Si en la entrada anterior expliqué como realizar validaciones de formularios en Tapestry en esta explicaré como crear nuevos validadores, como crear macros de validadores y como cambiar el decorador por defecto para mostrar los errores como deseemos. Estas tres cosas ayudará a las aplicaciones tengan menos código duplicado, el comportamiento sea consistente en toda la aplicación y por tanto que el mantenimiento sea más sencillo cuando haya que hacer cambios.

Crear nuevos validadores

Si los validadores por defecto de Tapestry no nos son suficientes necesitaremos crear algunos especificos para nuestras necesidades. Un ejemplo podría ser un campo donde permitamos introducir una cadena que tiene que pertenecer a un grupo de valores. Como es habitual en Tapestry siempre que necesitamos algo nuevo debemos implementar una interfaz o extender una clase. Para este caso podríamos implementar la interfaz Validator pero también es habitual que dispongamos de una clase abstracta de la que podamos extender e implemente la interfaz que necesitamos. Para un nuevo validador podemos utilizar AbstractValidator. Partiendo del código de uno de los validadores que ya disponemos por defecto, podemos hacer uno nuevo para un valor que tiene que estar dentro de un grupo.

public class In extends AbstractValidator<String, String> {
    public In() {
  super(String.class, String.class, "in");
 }

 public void validate(Field field, String constraintValue, MessageFormatter formatter, String value) throws ValidationException {
  List values = Arrays.asList(constraintValue.split("|"));
  if (!values.contains(value))
   throw new ValidationException(buildMessage(formatter, field, constraintValue));
 }

 public void render(Field field, String constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport) {
  formSupport.addValidation(field, "in", buildMessage(formatter, field, constraintValue), constraintValue);
 }

 private String buildMessage(MessageFormatter formatter, Field field, String constraintValue) {
  return formatter.format(constraintValue, field.getLabel());
 }
}

Una vez que tenemos la clase validadora tenemos que dar a conocer a Tapestry de su existencia, para ello debemos hacer una contribución en el contenedor de dependencias:

// ...en la clase del módulo de la aplicación

    public static void contributeFieldValidatorSource(MappedConfiguration<String, Validato> configuration) {
        configuration.add("in", new In());
    }
...

En este momento la podemos usar en los campos de la siguiente forma:

<t:label for="telefono">: <t:textfield t:id="telefonoField" value="producto" validate="in=coche|casa|televisión|libro" label="Producto"/>

Como cambiar el decorador por defecto

Cuando se producen errores de validación, Tapestry se encarga de decorar los campos para marcarlos como que tienen errores para de tal forma que el usuario pueda actuar en consecuencia, esto se hace sin que tengamos que hacer nada por nuestra parte. El decorador por defecto cuando hay errores marca la etiqueta y el campo en rojo y añade una imagen con una equis roja después del campo. Eso puede no ser lo que queramos ya que la imagen de la equis puede hacer que se desmaqueten las cosas si estamos justos de espacio en la pantalla.

Podemos personalizar la forma en que Tapestry decora los campos por defecto añadiendo elementos antes y despues de la etiqueta del campo y del campo mismo, también podemos añadir una clase de error a la etiqueta del campo y al campo de datos. Para ello debemos extender de la clase BaseValidationDecorator que nos proporciona los métodos para realizar las personalizaciones.

En este ejemplo añadimos una clase CSS a la etiqueta y al campo cuando el valor del campo no es válido. Tendremos que modificar el estilo de esa clase CSS que añadimos («t-error») para modificar su aspecto.

...

public final class AppValidationDecorator extends BaseValidationDecorator {

 private final Environment environment;

 private final MarkupWriter markupWriter;

 /**
  * @param environment
  *            used to locate objects and services during the render
  * @param markupWriter
  */
 public AppValidationDecorator(Environment environment, MarkupWriter markupWriter) {
  this.environment = environment;
  this.markupWriter = markupWriter;
 }

 @Override
 public void insideField(Field field) {
  if (inError(field))
   addErrorClassToCurrentElement();
 }

 @Override
 public void insideLabel(Field field, Element element) {
  if (field == null)
   return;

  if (inError(field))
   element.addClassName(CSSClassConstants.ERROR);
 }

 private boolean inError(Field field) {
  ValidationTracker tracker = environment.peekRequired(ValidationTracker.class);

  return tracker.inError(field);
 }

 private void addErrorClassToCurrentElement() {
  markupWriter.getElement().addClassName(CSSClassConstants.ERROR);
 }
}

Modificar el deccorador por defecto que usa Tapestry por defecto por el nuestro nos requerirá que modifiquemos la factoría que se encarga de devolver los decoradores.

public class AppValidationDecoratorFactory implements ValidationDecoratorFactory {
 private final Environment environment;

 public AppValidationDecoratorFactory(Environment environment) {
  this.environment = environment;
 }

 public ValidationDecorator newInstance(MarkupWriter writer) {
  return new AppValidationDecorator(environment, writer);
 }
}

Solo nos quedaría hacer la contribución al contenedor de dependencias para usar nuestra factoría en vez la de por defecto.

// ...en la clase del módulo de la aplicación

public static void bind(ServiceBinder binder) {
    binder.bind(ValidationDecoratorFactory.class, AppValidationDecoratorFactory.class).withId("AppValidationDecoratorFactory");
}

De esta forma podemos cambiar en todos los formularios de la aplicación la forma en que se muestran los mensajes de error de una forma muy simple. Se puede apreciar que toda la lógica de como se muestran los campos con error está en una única clase.

Macros de validaciones

Si nos encontramos en la situación en que un determinado conjunto de validaciones se repite a lo lago de los campos de la aplicación podemos definir una macro con ese conjunto de validaciones, como por ejemplo podría ser la validación de un campo contraseña.

// ...en la clase del módulo de la aplicación

@Contribute(ValidatorMacro.class)
public static void contributeValidatorMacro(MappedConfiguration configuration) {
      configuration.add("password","required,minlength=5,maxlength=15");
}

Ahora podemos usar el validador password, que comprende que el campo sea requerido, de una longitud mínima de 5 caracteres y de 15 como máximo, como si fuera un validador más de forma declarativa:

<input t:type="textField" t:id="password" t:validate="password" />

O mediante anotaciones:

@Validate("password")
private String password;

Referencia:
http://tapestry.apache.org/forms-and-validation.html
http://tapestry.apache.org/bean-validation.html
Validaciones de datos de formularios con Apache Tapestry
Documentación sobre Apache Tapestry