UserException en Wicket

Validaciones y UserException

Como ya vimos al trabajar en Arena, las validaciones son parte central del dominio. El negocio es entonces una suma de: entidades, relaciones entre ellas, comportamiendo, pero también reglas sobre todo esto. Por ejemplo "un alumno no puede estar inscripto a más de un curso el mismo día y mismo horario", o bien "la fecha de nacimiento no puede ser mayor a la fecha actual del sistema", etc.

Vimos también que, si bien el punto de entrada al sistema será la interfaz de usuario, y por lo tanto, como respuesta a la interacción de éste, se deberán ejecutar las validaciones, NO está bueno por ese motivo "correr" o codificar las reglas de negocio en la vista.

Es siempre importante mantener las reglas en los objetos de negocio.

La forma que establecimos para comunicar entonces una regla que falla en el dominio hacia la vista, era con excepciones particulares que llamábamos UserExceptions.

Así el dominio podía lanzar esta excepción, en dos lugares:

  • setter de propiedad: cuando se está intentando ingresar un valor incorrecto para una propiedad
  • métodos que representan operaciones: cualquier otro método que invocábamos desde la UI, por ejemplo desde un Button. En este caso podemos validar no solo una propiedad, sino todo el objeto.

UserException y Wicket

En Arena entonces la vida era fácil. Nosotros simplemente lanzábamos la UserException en nuestros setters o en los métodos y solito Arena sabía mostrar ese error de la mejor manera (o bueno, de una manera que seguramente se podrá mejorar :P)

Ahora, qué sucede si queremos utilizar el mismo modelo que teníamos, ahora en una aplicación Wicket ?

  • ¿Qué sucede si un método lanza una UserException? -> ¡Se rompe!
  • ¿Qué sucede si un setter lanza una UserException ? -> ¡Se rompe también!

Esto es lógico, porque WIcket es un framework de propósito general, que no conoce nuestras clases, ni tampoco conoce a la UserException. Con lo cual no sabe tratarla de diferente manera.

Lo que necesitamos hacer es adaptar Wicket para que entienda estas UserExceptions.

Veamos cada caso:

UserException en un Botón

El primer caso es, tengo un botón que invoca a un método del dominio. Éste método puede lanzar UserException. La primer solución sería así, por ejemplo para la calculadora que divide, el "divisor" no puede ser el número cero (0) con lo cual el dominio dice:

@Accessors
class CalculadoraDivision implements Serializable {
    double dividendo
    double divisor
    double resultado

    def void dividir() {
        validarDivisor(this.divisor)
        this.resultado = this.dividendo / this.divisor
    }

    def void validarDivisor(Double unDivisor) {
        if (unDivisor == 0) {
            throw new UserException("No se puede dividir por cero!")
        }
    }

Y desde la página, tenemos el botón que deberá manejar esta exception, caso contrario va a aparecer un error feo.

def addActions(Form<CalculadoraDivision> form) {
    val button = new XButton("dividir")
    button.onClick = [| 
        try { 
            form.modelObject.dividir 
        } catch (UserException e) {
            error(e.message)
        }
    ]
    form.addChild(button)
}

Como vemos acá, hay que catchear la exception y luego, todos los controles (en este caso Button) tienen un método error(...) que permite avisarle a wicket que se produjo un error. Esto hará que wicket se acuerde del error, y quede en la misma página.

Para que el error se vea en pantalla, necesitamos en nuestra página, tener un FeedbackPanel:

def addFields(Form<CalculadoraDivision> form) {
    form.addChild(crearDividendoTextField(form))
    form.addChild(crearDivisorTextField(form))
    form.addChild(new Label("resultado"))
    form.addChild(new FeedbackPanel("feedbackPanel"))
}

En nuestro caso se verá algo así ahora:

Ahora, es muy probable que necesitemos este patrón de try-catch (UserException) en cada uno de nuestros botones de la aplicación. Entonces, si seguimos el principio DRY , podemos crear una nueva clase Button que haga esto por nosotros, y ahora todos nuestros botones van a extender de esta clase, en lugar de la de wicket original.

Acá hay un ejemplo en Java:

public abstract class SmartButton extends Button {

    protected SmartButton(String id) {
        super(id);
    }
    
    @Override
    public void onSubmit() {
        try {
            this.onSmartSubmit();
        }
        catch(UserException e) {
            error(e.getMessage());
        }
    }

    protected abstract void onSmartSubmit();
    
}

Como verán es una clase abstracta, ya que no sabe realmente qué hay que hacer, solo sabe que al hacerlo, deberá manejar las UserException. Con lo cual define un nuevo método onSmartSubmit() abstracto que cada subclase deberá implementar.

Ahora en la calculadora tendremos esto:

def addActions(Form<CalculadoraDivision> form) {
    val button = new XSmartButton("dividir")
    button.onClick = [| form.modelObject.dividir ]
    form.addChild(button)
}

Que es muy similar a lo que hacíamos antes cuando no manejábamos la exception pero, sí la maneja. Sin tener que repetir el try-catch.

UserException en un setter (Formulario)

Bien, con nuestro nuevo botón ahora podremos capturar las UserExceptions lanzadas en los métodos de dominio, que claro, se invocan desde el botón. En general nuestros botones entonces son parte de un formulario, y se ejecutan ante un submit.

El usuario ingresa los datos y le da click al botón. Eso termina invocando al método de dominio.

Sin embargo todavía nos queda un caso que Wicket no sabe manejar: las UserExceptions lanzadas desde los setters.

Veamos, podríamos modificar nuestra calculadora para implementar la validación en el setter. Así, la regla es la misma, pero nuestro modelo no va a permitir nunca entrar en un estado inválido. A diferencia del código que teniamos antes, que fallaba al dividir, ahora falla mucho antes al momento de setearle el divisor.

    def void setDivisor(double divisor) {
        if (divisor == 0) {
            throw new UserException("No se puede dividir por cero!")
        }
        this.divisor = divisor
    }

Detrás de este cambio que hicimos, hay que entender que hay una abstracción. La regla de negocio "el divisor no puede ser 0" la identificamos como una constraint o regla sobre la propiedad "divisor" de nuestra CalculadoraDivision. Antes más bien era como un chequeo dentro del método "dividir" (también podríamos pensarla como una pre-condición de llamado al dividir()).

Bien, como la regla vemos que se basa 100% en la property, qué mejor que tenerla en la misma property, es decir en su setter.

Ahora, ¿por qué decimos que Wicket no va a poder manejar esto con nuestro nuevo Botón? Porque lógicamente antes de invocar nuestro onSubmit() del botón, wicket se encarga primeramente de actualizar el modelo en base a los datos ingresados por el usuario. Y para eso, invoca los setters. Si un setter lanza una exception, la ejecución termina abruptamente ahí y nunca llega a nuestro botón.

El siguiente diagrama resume los pasos de procesamiento de un Form:

En este caso el paso "Push input (to models)" es el que intenta actualizar el modelo con los datos que llegan del request. Más adelante veremos para qué sirven los pasos anteriores. Entonces, ¿cómo hacemos para que wicket al intentar llenar los datos en el modelo se "avive" de que UserException es una excepción de dominio por la que debe avisar al usuario?

De nuevo lo más prolijo es hacer una nueva clase Form nuestra que sepa manejar las excepciones de negocio y las otras también. 

class FormCopado extends Form {

    new(String id, IModel model) {
        super(id, model)
    }

    override def process(IFormSubmitter submittingComponent) {
        try {
            super.process(submittingComponent)
        } catch(WicketRuntimeException e) {
            if (e.cause instanceof UserException) {
                this.error(e.cause.message)
            } else {
                throw e
            }
        }
    }
}

Es un poquito más complicado el catch, ya que wicket wrappea (envuelve) la excepción original lanzada por el dominio (setter) en otra propia de wicket (WicketRuntimeException) con lo cual tenemos que atrapar esta última y determinar la causa (cause es una propiedad que referencia a la excepción de origen si existe). Recién ahí sabemos si se trata o no de una UserException. De serlo, le indicamos a wicket que hubo un "error". Caso contrario, no sabemos qué excepción es, así que mejor la dejamos como está, relanzándola.

Ahora sí, las excepciones en los setters se van a ver bien en los feedback panels.

Otros mecanismos de validación en Wicket

Seguir por esta otra página para ver el mecanismo de validación de formularios en wicket