public interface Validator<T> {
public ValidationResult validate(T candidate);
}
Neste workshop veremos como criar, passo a passo, sua própria API de validação. Para isso definiremos primeiro as propriedades de uma API deste tipo e utilizaremos o padrão Composite Object para aumentar a reutilização da API.
Validar significa testar se algo é o que deveria ser. Normalmente significa verificarque um dado valor é diferente de algum valor fixo ou está em um intervalo de valores.
O resultado de uma validação não é a apenas a indicação de que o valor está valido ou não, mas principalmente a razão de porquê não está válido. Isto significa que nossos validadores não podem simplesmente retornar "sim" ou "não" mas terão que retornar um objeto que informe porque o valor é invalido. Se for válido, obviamente não precisamos informar mais nada.
O nosso modelo é bem simples. Definimos um validador como uma interface a ser implementada conforme a lógica de validação. O validador têm um método validate
que recebe o objeto a ser validado e retorna o resultado da validação no objeto ValidationResult
.
Faremos o validador aceitar um objeto genérico qualquer fortemente tipado. Veremos depois a utilidade desta funcionalidade e o porquê de não usar Object
diretamente.
public interface Validator<T> {
public ValidationResult validate(T candidate);
}
Como vimos, apenas precisamos de informar as razões de porquê o valor não é válido. Precisamos apenas acrescentar um objeto InvalidationReason
ao resultado, tornando o objeto ValidationResult
uma coleção de InvalidationReason
.
public class ValidationResult implements Iterable<InvalidationReason> {
private final List<InvalidationReason> reasons = new LinkedList<InvalidationReason>();
public Iterator<InvalidationReason> getIterator(){
return reasons.iterator();
}
public void addReason(InvalidationReason reason){
reasons.add(reason);
}
public void removeReason(InvalidationReason reason){
reasons.remove(reason);
}
}
Contudo, apenas a razão não é suficiente. Imaginemos uma situação em que o validador não consegue decidir se o valor é válido ou não. Por exemplo, um validador de e-mail tenta connectar-se a um servidor DNS para verificar que o domínio do e-mail existe. Se o validador não poder fazer essa ligação, ele não pode dizer que o e-mail é válido, mas também não pode dizer que não é. Nesta circunstância o validador precisa informar a dúvida. A forma de fazer isso é definir um grau de severidade para a razão de invalidação.
public interface InvalidationReason {
public InvalidationSeverity getSeverity();
public String getMessage();
public Object[] getParams();
}
public enum InvalidationSeverity {
SEVERE,
WARNING
}
Definimos InvalidationReason
como uma interface para termos o máximo de flexibilidade na implementação. O método getMessage()
retorna uma mensagem e um método getParams
os parâmetros da mensagem. Isto serve para podermos criar mensagem corretamente internacionalizadas e deixa a UI se preocupar com o texto real e sua apresentação.
Faltou apenas decidir como sabemos que o valor está válido ou não. Sabemos que ele não está válido se existir uma razão severa para a invalidação. Então, em vez de verificarmos isso a todo o momento, vamos criar um método, isValid()
, que faz esse trabalho.
public class ValidationResult implements Iterable<InvalidationReason> {
private final List<InvalidationReason> reasons = new LinkedList<>();
// o mesmo codigo de antes
public boolean isValid(){
for (InvalidationReason reason : reasons){
if (reason.getSeverity().equals(InvalidationSeverity.SEVERE){
return false;
}
}
return true;
}
}
Este método obriga a iterar todas as razões para verificar se existe alguma com severidade SEVERE
. Não é muito eficaz. Se tivéssemos um Map
seria bem mais facil.
EnumMap
Refactoremos então nossa classe ValidationResult
para utilizar um Map
de enumerações. Felizmente a API padrão nos oferece EnumMap
já pronto para esse uso. A ideia é termos um mapa onde a chave é a severidade e o valor do mapa é a lista das razões.
public class ValidationResult implements Iterable<InvalidationReason> {
private final EnumMap<InvalidationSeverity, List<InvalidationReason>> reasons
= new EnumMap<>(InvalidationReason.class);
private List<InvalidationReason> getReasonsList(InvalidationSeverity severity){
List<InvalidationReason> list = reasons.get(severity);
if (list == null){
list = new LinkedList<InvalidationReason>();
reasons.put(severity,list);
}
return list;
}
public Iterator<InvalidationReason> getIterator(){
return getAllReasons().iterator();
}
public List<InvalidationReason> getSevereReasons(){
return Collections.unmodifiableList(getReasonsList(InvalidationSeverity.SEVERE));
}
public List<InvalidationReason> getWarningReasons(){
return Collections.unmodifiableList(getReasonsList(InvalidationSeverity.WARNING));
}
public List<InvalidationReason> getAllReasons(){
List<InvalidationReason> all = new ArrayList<InvalidationReason>();
all.addAll(getReasonsList(InvalidationSeverity.SEVERE));
all.addAll(getReasonsList(InvalidationSeverity.WARNING));
return all;
}
public void addReason(InvalidationReason reason){
getReasonList(reason.getSeverity()).add(reason);
}
public void removeReason(InvalidationReason reason){
getReasonList(reason.getSeverity()).remove(reason);
}
public boolean isValid(){
return getReasonsList(InvalidationSeverity.SEVERE).isEmpty();
}
public boolean hasReasons(){
return !(getReasonsList(InvalidationSeverity.SEVERE).isEmpty()
&& getReasonsList(InvalidationSeverity.WARNING).isEmpty()
);
}
}
Com esta alteração precisamos de um método auxiliar getReasonsList
que pesquisa a lista de razões para a severidade correta. Adicionamos também métodos que buscam apenas as razões de uma dada severidade, mas não permitimos que essas listas sejam manipuladas directamente. Os métodos addReason
e removeReason
devem ser usados para isso.
O método isValid
é agora mais rápido. Adicionamos também o método hasReasons
para testar se existe alguma razão de invalidação no resultado.
Temos todas as peças para tentarmos implementar um validador. Implementaremos um validador para String
s que valida se ela não está vazia. A String
não está vazia se for diferente de null
e tiver tiver pelo menos um caracter diferente de espaço.
public class EmptyValidator implements Validator<String>{
public ValidationResult validate(String object){
ValidationResult result = new ValidationResult();
if (object == null || object.trim().isEmpty()){
// invalido
result.addReason(new StringInvalidationReason(
InvalidationSeverity.SEVERE, "string.isEmpty"
));
}
return result;
}
}
A lógica é simples. Precisamos criar um objeto para o resultado da validação. Fazemos um teste usando if
que deteta que o valor não é válido e retornamos o resultado. Criamos uma razão de invalidação e a adicionamos ao resultado. A classe StringInvalidationReason
é a simples implementação da interface InvalidationReason
. Deixo sua implementação à sua responsabilidade como exercício.
Com este mecanismo básico você pode implementar validadores para os tipos que quiser com as validações que quiser, como o famoso validador de CPF ou de CNPJ.
Um tipo de validador mais interessante é aquele que precisa de parâmetros. Um exemplo clássico seria verificar se um numero ou uma data está em determinado intervalo, para isso precisamos informar os limites desse intervalo. Eis como poderíamos fazer isso para a validação de datas.
public class DateIntervalValidator implements Validator<Date>{
private final Date minDate;
private final Date maxDate;
public DateIntervalValidator(Date minDate, Date maxDate){
this.minDate = minDate;
this.maxDate = maxDate;
}
public ValidationResult validate(Date date){
ValidationResult result = new ValidationResult();
if (date==null || date.before(minDate)){
result.addReason(new StringInvalidationReason(
InvalidationSeverity.SEVERE, "date.outofrange.min", minDate
));
}
if (date==null ||date.after(maxDate) ){
result.addReason(new StringInvalidationReason(
InvalidationSeverity.SEVERE, "date.outofrange.max", maxDate
));
}
return result;
}
}
Este exemplo mostra como utilizar parametros para a validação e como validar mais do que um teste. Repare que o teste dá como invalida uma data que seja null
e nesta situação ambas a razões serão adicionadas ao resultado. Caberá à UI decidir qual escolher e apresentar a mensagem, ou executar um outro validador antes para verificar que a data não é nula.
Vimos como criar validadores que testam uma única regra. Contudo, a maior parte das vezes precisamos validar o mesmo valor com diferentes regras. Poderiamos criar um validador com imensas regras lá dentro, mas seria muito imprático de usar. Fora que se quiseremos validar apenas uma regras simples, esse validador seria um monstro.
A opção é criar um validador que agrupa outros validadores. Para isso seguiremos o padrão Composite Object. A validação do validador composto resume-se apenas a executar a validação dos seus validadores "filhos".
public class CompositeValidator<T> implements Validator<T>{
private final List<Validator<T>> validators = new LinkedList<Validator<T>>();
public CompositeValidator(){}
public CompositeValidator<T> add(Validator<T> validator){
validators.add(validator);
return this;
}
public CompositeValidator<T> remove(Validator<T> validator){
validators.remove(validator);
return this;
}
public ValidationResult validate(T object){
ValidationResult finalResult = new ValidationResult();
for (Iterator<Validator<T>> it = validators.iterator(); finalResult.isValid() && it.hasNext();){
Validator<T> validator = it.next();
ValidationResult result = validator.validate(object);
finalResult.addResult(result);
}
return finalResult;
}
}
Basta que o validador composto itere os validadores internos até encontrar uma razão de invalidação. Contudo, se você preferir pode deixá-lo executar todos os validadores. O método addResult
foi adicionado em ValidationResult
para permitir juntar a um resultado as mensagens de outro. A implementação é a simples copia dos objetos entre as coleções internas. Fica também como exercicio.
Outro ponto a ressaltar é o uso do padrão Method Channing nos métodos add
e remove
para facilitar a montagem do validador composto.
Vimos como criar validadores que testam um valor, e vimos como agrupar vários validadores. Contudo, o que seria realmente util seria validar todos as propriedades de um bean (ver padrão PropertyBag ).
Para isso teremos que definir qual validador valida qual propriedade. Evidentemente teremos que utilizar de algumas funcionalidades de introspeção para podermos ler os valores das propriedades. Seguindo o padrão cada propriedade deve ter pelo menos um método acessor (meétodo get
).
A implementação é semelhante ao do validador composto já que, afinal, o validador de beans também é composto de validadores, a diferença é que precisamos associar um nome ao validador.
public class BeanValidator<T> implements Validator<T> {
private final Map<String, Validator> validators = new HashMap<>();
public BeanValidator addPropertyValidator(String propertyName, Validator<?> validator){
validator.put(propertyName, validator);
return this;
}
public BeanValidator removePropertyValidator(String propertyName){
validator.remove(propertyName);
return this;
}
public ValidationResult validate (T bean){
ValidationResult finalResult = new ValidationResult();
for (Map.Entry<String,Validator>> entry : validators.entrySet()){
Object value = readProperty(entry.getKey(), bean);
ValidationResult result = entry.getValue().validate(value);
finalResult.addResult(result);
}
return finalResult;
}
private Object readProperty (String propertyName, Object bean){
try {
Method method = bean.getClass().getMethod("get" + propertyName );
return method.invoke(bean);
} catch(Exception e){
throw new RuntimeException(e);
}
}
}
A validação se resume a iterar todos os pares propriedade-validador. O método readProperty
é uma método auxiliar para ler o valor da propriedade a partir do bean usando introspeção.
Esta é uma implementação simples de um validador de bean. Poderiamos incrementar usando anotações, por exemplo, de forma a definir os validadores automáticamente a partir de anotações como @NotNull
, @InRange
, etc.
Finalmente temos todas as peças de uma API de validação. O Validador, a razão da invalização, o resultado da validação, o validador composto para agruparmos validadores e o validador de bean. Agora que entendeu o mecanismo vamos ver um exemplo de uso.
BeanValidator pessoaValidator = new BeanValidator<Pessoa>()
.addPropertyValidator("nome", new CompositeValidator() // nome
.add(new EmptyValidator()) // não pode ser vazio
.add(new MaxLengthValidator(100)) // nem maior que 100 caracteres
).addPropertyValidator("cpf", new CompositeValidator() // cpf
.add(new NullValidator()) // não pode ser nulo
.add(new CpfValidator()) // e tem que respeitar as regras de digito verificador
).addPropertyValidator("nascimento", new CompositeValidator() // data de nascimento
.add(new NullValidator()) // não pode ser nula
.add(new InRangeValidator(null, new Date())) // e tem que ser menor que hoje
)
Pessoa p = new Pessoa();
if( pessoaValidator.validate(p).isValid() ){
System.out.println("Valido");
} else {
System.out.println("Inválido");
}
O código acima resulta em inválido pois nenhum campo foi preenchido. Ele mostra apenas como montar o validador e executá-lo.
A esta hora é mais obvio porque um mecanismo usando anotations é preferido, contudo nem sempre queremos encher nossos beans com um monte de anotações. Além disso poderiamos desenvolver um objeto Builder para simplificar o processo de definição dos validadores. Algo como:
Validator <Pessoa> pessoaValidator = ValidatorBuilder.forBean(Pessoa.class)
.validate("nome").notNull().and().maxLength(100)
.validate("cpf").notNull().and().validateWith(new CpfValidator())
.validate("nascimento").notNull().and().lessThan(new Date())
.build();
Pessoa p = new Pessoa();
if( pessoaValidator.validate(p).isValid() ){
System.out.println("Valido");
} else {
System.out.println("Inválido");
}
Vimos como é implementada uma API de validação. Vimos que preciamos de um objeto que contém a regra de validação ( Validator
), um que é resultado do processo de validação ( ValidationResult
) e um objeto que represente as razões para a invalidação ( InvalidationReason
).
Depois contruimos um validador que permite compor vários validadores tirando partido das regras já escritas e permitindo montar validadores mais complexos. Finalmente criámos um validador simples capaz de validar beans.
Muito mais pode ser feito para melhorar a API, como por exemplo, ter validadores no bean validator que testam todo o bean depois que todos os campos foram validados. Um outro tipo de validador possivel é um que valide coleções validando relações entre os elementos.
[1] Swing Data Validation, Karsten Lentzsch (http://www.jgoodies.com/articles/validation.pdf)