Hibernate ValidatorA base para o JSR 303: Bean Validation.
AutorClayton K. N. Passos (netstart[@]gmail.com): Viciado em tecnologia. Atualmente trabalha como Desenvolvedor/Analista Pleno. (http://japz-j.blogspot.com)
A validação de dados pode e deve ser um processo centralizado e flexível o bastante para ser desempenhando em diferentes camfadas de um sistema, mas de forma centralizada. Utilizando o Hibernate Validator, inspiração para a JSR 303, isto pode ser uma tarefa simples.
IntroduçãoValidação de dados é uma tarefa comum, muitas vezes implementada em diferentes camadas, de diversas maneiras e até mesmo separado do framework de validação, consumindo tempo e causando erros difíceis de serem encontrados pela falta de centralização dos interesses (concerns).
Vários projetos vêm implementando e apresentando suas idéias no que diz respeito à validação de dados, entre eles: Struts, Spring e Hibernate. Neste artigo falaremos sobre a forma proposta pelo Hibernate 3, que é uma das inspirações para a JSR 303, uma das novas especificações prevista para o Java 7.
Nas primeiras versões do Hibernate 3, a API de validação estava junto do sub-projeto Hibernate Annotations, mas no momento da escrita deste artigo esta API encontra-se separada e leva o nome Hibernate Validator. Vale ressaltar que para a validação dos recursos aqui apresentados, utilizamos as próprias classes de teste do Hibernate Validator com algumas pequenas modificações e também os softwares e APIs nas seguintes versões:
· Hibernate Validator 3.0.0.GA;
· Hibernate Core 3.2.4.GA;
· Hibernate-Annotations 3.3.0.GA
· Eclipse 3.2.2;
· Java 1.6.0_01-b06.
Hibernate ValidatorSeguindo o princípio da filosofia DRY (Don’t Repeat YourSelf), o Hibernate Validator trás uma forma de adicionar regras para validação automática de dados, de maneira que estas validações sejam implementadas uma e somente uma vez em toda a aplicação, eliminando a duplicação destas regras entre as diversas camadas (presentation, data access, database schema).
Utilizar annotations para adicionar regras é bastante conveniente e elegante, com elas podemos definir várias regras diretamente no modelo de classes persistente. O Hibernate Validator (H.V.) possuí algumas validações comuns (@NotNull, @Email, @Max, @Length, entre outras), mas não prende o desenvolvedor, possibilitando, a criação de validações personalizadas e internacionalizadas.
Cada annotation é associada a uma implementação de validação, que verifica se a instancia da entidade obedece a sua regra. O Hibernate faz isto automaticamente antes de um insert ou update, mas você também pode chamar a validação a qualquer momento em sua aplicação e de qualquer Java Persistence provider (JPA).
As validações podem ser utilizadas em dois níveis: aplicação e banco de dados. A primeira traz a possibilidade de validar a instancia de classes através de suas regras em tempo de execução (veja o quadro de constraints), enquanto que a segunda é incorporada ao Hibernate metamodel e ao database schema gerado, traduzindo as regras que você adicionou no mapeamento de suas entidades. Por exemplo, se você adicionar @NotNull a uma propriedade de sua entidade, a coluna irá ser declarada como not null no DDL schema gerado pelo Hibernate (hbm2ddl). Caso você não queria mais este comportamento, basta atribuir false a hibernate.validator.apply_to_ddl.
Este artigo está focado na primeira forma de validação.
ConstraintsAqui você pode ver uma lista de algumas regras que podem ser adicionadas a suas entidades, de maneira que o hibernate possa validar as informações armazenadas dentro delas. Lembre-se de que estas são constraints que o Hibernate Validator fornece para nós trabalharmos, mas podemos definir as nossas constraints personalizadas. Mas o que são constraints?Em poucas palavras: Constraints são regras de restrição, e sua semântica é definida pelas annotations que a descrevem.
Annotation Funcionalidade
@Length(min=, max=): Verifica se o tamanho da string esta dentro dos limites.
@Max(value=): Verifica se o valor é maior ou igual. Aplicado a representações de números, armazenados até mesmo em String.
@Min(value=): Verifica se o valor é menor ou igual. Aplicado a representações de números, armazenados até mesmo em String.
@NotNull: Checa se o valor não é null.
@NotEmpty: Checa se não é null e não vazia.
@Past: Checa se uma data esta no passado.
@Future: Checa se uma data esta no futuro.
@Pattern(regex=”regexp”, flag=)ou@Patten({@Pattern(...)}): Checa se a propriedade obedece à expressão regular.
@Range(min=, max=): Confere se o valor está entre o mínimo e o máximo (incluindo), aplicado a valores numéricos, mesmo armazenados em String.
@Size(min=, max=): Confere se a quantidade de elementos esta entre o mínimo e o máximo (incluído), aplicado a Array, Collection, Map.
@AssertFalse: Confere se o valor de uma propriedade boolean é false.
@AssertTrue :Confere se o valor de uma propriedade boolean é true
@Valid: Impõe uma validação recursiva aos objetos associados. Se o objeto é uma Collection, Map ou um Array, os elementos são validados recursivamente.
@Email: Confere se a string é um e-mail válido.
@CreditCardNumber: Confere se a string é um número de cartão de crédito válido.
@Digits: Verifica quantidade de dígitos ante e depois do separador de casa decimal (integerDigits e factionalDigits).
@EAN: Verifica se a string é um código EAN ou UPC-A válido
Como utilizar o framework?É chegada a hora de “sujarmos as mãos” e ver como a teoria se aplica na prática. Lembre-se de que se você estiver utilizando o Hibernate como uma solução para o Mapeamento Objeto Relacional (ORM), a integração do Hibernate Core com o Hibernate Validador é transparente. As validações ocorrem sempre antes de qualquer insert ou update, caso haja algum erro será disparado uma exceção com as regras violadas. Se por algum motivo for necessário desabilitar a esta integração basta atribuir false a hibernate.validator.autoregister_listeners.
Para começar vamos criar uma classe que servirá apenas para nos mostrar os erros de uma forma mais amigável. Chamaremos esta classe de HandlerTest e seu propósito de existência é o de imprimir na saída padrão as mensagens de erro, se tiver alguma, caso contrário deverá nos avisar de que não há nenhum erro, veja a listagem 1.
Analisando o código, vemos que a classe HandlerTest verifica se o array de InvalidValue é igual a 0, esta é a forma da qual você pode ter certeza de que não nenhum erro foi detectado pelo Hibernate Validator, pois, sempre que for detectado um erro, haverá algum elemento no array retornado pelo validador. Caso o array invalidValues tenha algum elemento o código da listagem 1 chama o método print() para nos mostrar o quê tem dentro do array. Veja o quadro InvalidValue para saber a utilidade dos principais métodos desta classe.
Listagem 1: Classe HandlerTest
public class HandlerTest {
public static void printErrors(InvalidValue[] invalidValues) {
System.out.println("** Listagem de erros **");
if (invalidValues.length == 0) {
System.out.println("Oba! Não há erros!\n");
} else {
print(invalidValues);
}
}
private static void print(InvalidValue[] invalidValues) {
Class clazz = invalidValues[0].getBeanClass();
System.out.print("Há " + invalidValues.length);
System.out.println(" erro(s) na entidade: " + clazz.getSimpleName());
for (InvalidValue iv : invalidValues) {
System.out.print("A propriedade ");
System.out.print(iv.getPropertyName());
System.out.println(" possui o seguinte erro: ");
System.out.println(iv.getMessage());
if (iv.getValue() != null) {
System.out.print("Valor errado: ");
System.out.println(iv.getValue());
}
}
}
}
InvalidValueGetBeanClass(): Retorna o tipo da classe que provocou o erro.
getBean(): Retorna a instancia da classe que falhou na validação.
getMessage(): Retorna a mensagem de erro, que pode estar internacionalizada.
getValue(): Retorna o valor errado.
Como já dissemos, você pode validar as informações contidas em suas entidades, em qualquer lugar do sistema. A maneira de fazer isto é chamando o método getInvalidValues da classe ClassValidator, se houver algum erro na entidade ele retornará um array de InvalidValue contendo diversas informações pertinentes a este erro. Simples, não?
Agora que já temos uma maneira simples de verificar as validações, vamos a um caso de uso fictício, suponha que para a compra de um carro o mesmo deve obedecer a dois critérios, seu IPVA deve estar pago e livre de amassados. Definimos classe Car utilizando as Annotations @AssertTrue e @AssertFalse de maneira que a propriedade ipvaPago seja igual a true, e que a propriedade amassado seja igual a false, veja a listagem 2.
Listagem 2: Classe Carro
public class Carro{
String cor;
@AssertTrue
boolean ipvaPago;
@AssertFalse
boolean amassado;
}
Na listagem 2 podemos observar que as constraints foram adicionadas em cima da propriedade, uma alternativa caso seja necessário é definidas sobre o método get da respectiva propriedade. Particularmente não gostamos de definir as regras em cima dos métodos getXX(), pois torna o código menos intuitivo e menos legível, aconselhamos que sempre utilize a propriedade para adicionar as constraints.
Acompanhe na listagem 3, que a classe AssertTest imprimirá primeiramente dois erros, mas posteriormente, como não será detectado nenhum erro pelo validador, apenas haverá uma mensagem confirmando que as informações contidas na entidade são válidas. Como não definimos nada no código referente a língua, as mensagens de erros virão na língua configurada no Sistema Operacional. Até o momento da escrita deste artigo, eram onze as línguas suportadas, entre elas: português do Brasil; inglês; espanhol; italiano; francês; chinês e alemão.
Lista 3: Classe AssertTest
public class AssertTest {
public static void main(String[] args) {
Car fuscao = new Car();
fuscao.cor = "preto";
fuscao.ipvaPago = false;
fuscao.amassado = true;
ClassValidator classValidator = new ClassValidator(Car.class);
InvalidValue[] invalidValues = classValidator.getInvalidValues(fuscao);
printErrors(invalidValues);
fuscao.ipvaPago = true;
fuscao.amassado = false;
invalidValues = classValidator.getInvalidValues(fuscao);
printErrors(invalidValues);
}
}
Definindo suas próprias validaçõesCriar nossas próprias validações é muito simples, é necessário apenas duas coisas, criar uma Annotation que descreva a regra e criar uma classe que implemente a interface org.hibernate.validator.Validator (veja a listagem 4 e 5), feito isto é só adicionar a Annotation a sua entidade para que ela seja validada.
Listagem 4: Annotation Phone
@ValidatorClass(PhoneValidator.class)
@Target({FIELD, METHOD})
@Retention(RUNTIME)
@Documented
public @interface Phone {
String ddd() default "44";
String message() default "{validator.phone}";
}
Na listagem 4, podemos acompanhar o código de uma Annotation destinada a validação de telefones. Três pontos importantes devem ser observados nesta classe. Na primeira linha temos a anotation @ValidatorClass dizendo que é utilizado a classe PhoneValidator como a implementação que irá validar a entidade anotada com @Phone. Os dois próximos pontos importantes são as propriedades ddd e message. Ambas podem ser mudadas no momento de definir uma entidade da seguinte forma:
@Phone(ddd = “45”, message = “mensagem de erro”)
A propriedade message tem propósito especifico, que é armazenar uma string que será utilizada caso um erro proveniente desta Annotation seja detectada. No exemplo, o valor default é {validator.phone}, uma mensagem nada inteligível, mas aliado ao recurso de internacionalização ela será, pois com esta chave o mecanismos de internacionalização do Hibernate Validator tentará encontrar a mensagem apropriada, caso não encontre será utilizado a string {validador.phone} na integra.
A classe PhoneValidator mostrada na listagem 5, possuí apenas dois métodos reescritos da interface Validator. Enquanto o método initialize(Phone parameters) recupera os valores atribuídos na entidade para a annotatoin @Phone o método isValid(Object value) faz o trabalho de verificar se o conteúdo da propriedade é valido, atente para o parâmetro que isValid recebe, esta é a informação que deve ser validada. Em nosso código de exemplo para um telefone ser válido é preciso estar no formado (44)0000-0000 (DDD 44 entre parênteses, seguido de quatro números, um traço e novamente quatro números).
O Hibernate Validator utiliza a classe ResourceBundle para internacionalizar as mensagens de erros. É bastante tranqüilo trabalhar com ela, primeiramente precisamos de um arquivo .properties, dentro dos padrões (veja o quadro: Um pouco sobre internacionalização); a Annotation precisa estar preparada para trabalhar com a mensagem internacionalizada e que seja explicitado que utilizaremos o arquivo específico para as mensagems.
Na listagem 6, temos um código completo, que imprime a mesma mensagem de erro em duas línguas diferente. Para isto foram necessárias apenas algumas linhas de código adicionais, veja que foi preciso passar no segundo parâmetro do construtor de ClassValidator um ResouceBundle que foi criado apartir de um Locale.
É possível sobrescrever as mensagens padrão do H.V., a regra é simples. Dentro do arquivo que você armazena as mensagens internacionalizadas, você adiciona a chave validator.nomedaannotation. Por exemplo, para mudar a mensagem da annotation @NotNull haverá no arquivo a seguinte chave:
validator.notnull=Erro, a propriedade não pode ser null
Se for necessário informar os valores permitidos, como é caso de @Length, podemos colocar o nome da variável entre chaves, desta forma, esta será trocada automaticamente pelo valor configurado na entidade. Veja o exemplo a seguir:
validator.length=Xii!! Deu erro no Length, os valores devem estar entre {min} e {max}
Listagem 5: Classe PhoneValidator
public class PhoneValidator implements Validator {
private String dddValid;
public void initialize(Phone parameters) {
dddValid = parameters.ddd();
}
public boolean isValid(Object value) {
if (value == null) {
return true;
}
String phone = (String) value;
//(44)9962-3019
String reg = "\\(" +dddValid+ "\\)[0-9]{4}\\-[0-9]{4}";
Pattern pattern = Pattern.compile(reg);
Matcher match = pattern.matcher(phone);
return match.matches();
}
}
Listagem 6: Classe PhoneTest
public class PhoneTest {
public static void main(String[] args) {
HomePhone hPhone = new HomePhone();
hPhone.phone = "3366-3330";
Locale ptBR = new Locale("pt", "BR");
ResourceBundle rBundleDefault = ResourceBundle.getBundle("messages", ptBR);
ResourceBundle rBundleEnglish = ResourceBundle.getBundle("messages", Locale.ENGLISH);
ClassValidator classValidator;
classValidator = new ClassValidator(HomePhone.class, rBundleEnglish);
InvalidValue[] invalidValues = classValidator.getInvalidValues(hPhone);
printErrors(invalidValues);
classValidator = new ClassValidator(HomePhone.class, rBundleDefault);
invalidValues = classValidator.getInvalidValues(hPhone);
printErrors(invalidValues);
hPhone.phone = "(44)9962-3019";
invalidValues = classValidator.getInvalidValues(hPhone);
printErrors(invalidValues);
}
}
Não se preocupeO Hibernate Validator cuida de vários problemas comuns do desenvolvimento de software. Referencia circular, muito discutido no quesito qualidade de software, é um destes problemas, este tipo de situação, comum, levou a alguns brasileiros criarem um pluguin para o Eclipse chamado ByeCycle que auxilia a eliminar referencias circulares. Acompanhe pela listagem 7 que a entidade Contact nos diz que a entidade Brother também deve passar pela validação (@Valid), mas a Classe Brother faz o mesmo para com a classe Contact, isto poderia facilmente levar a um problema sério de referencia circular, mas isto não é algo do qual devemos nos preocupar, pois o H. V. sabe lidar com esta situação perfeitamente, nos permitindo usar estas classes.
Listagem 7: Referencia circular
public class Contact {
@NotNull
public String phone;
@Valid
public Brother brother;
}
public class Brother {
@NotEmpty
public String name;
@Valid
public Contact contact;
}
É possível validar todos os elementos de uma Collection ou array facilmente, não é necessário nenhum recurso “informágico”. Basta adicionar a Annotation @Valid a estrutura que esta terá todos os seu itens validados. Acompanhe a listagem 8 que demonstra como uma classe TV pode ter o seu Map de Show verificado item a item pelo Hibernate Validator.
Listagem 8: iterable
public class Tv {
@NotNull
public String name;
@Valid
public Map shows = new HashMap();
}
public class Show {
@NotNull
public String name;
}
Esta poderosa ferramenta nos permite também agregar várias expressões regulares, para validar uma única propriedade, acompanhe a listagem 9, observe que definimos duas E.R. e também personalizamos as mensagens que serão utilizadas para informar o usuário caso alguma destas regras sejam desrespeitadas.
Listagem 9: agregando E.R.
@Patterns( {
@Pattern(regex = "^[A-Z0-9-]+$", message = "must contain alphabetical characters only"),
@Pattern(regex = "^....-....-....$", message = "must match ....-....-....") }
)
Integração com HibernateA utilização do Hibernate (core) propriamente dito em conjunto com o Hibernate Validator é feita através do Hibernate Annotation (ou Hibernate EntityManager) e neste caso a validação será automática, não será necessário nenhum código para chamar o validador, pois esta chamada ocorrerá antes de qualquer inserção de dados novos ou alterações (save ou update).
Para verificar este comportamento foi criado uma entidade chamada Linha, a ela demos alguns atributos e algumas regras para serem verificadas, como por exemplo @NotNull e @Length(max = 100). Também foram criadas as classes HibernateUtil e LinhaTeste, classes utilizadas para nos fornecer a Session e testar o comportamento de validação respectivamente.
Após a execução do teste dois tipos de exceções foram lançadas: org.hibernate.PropertyValueException e org.hibernate.validator.InvalidStateException ambas indicando problemas encontrados pelo Hibernate Validator, veja a listagem 11 e 12 para verificar um pequeno trecho da exceção.
Não é recomendável mas caso seja necessário, é possível desligar a pré validação atribuindo false a variável de configuração:
hibernate.validator.autoregister_listeners
Listagem 10: Classe LinhaTeste
public static void main(String[] args) {
Linha linha = new Linha();
linha.setIdLinha(new BigDecimal(10));
Session session = HibernateUtil.getSession();
session.getTransaction().begin();
session.save(linha);
session.getTransaction().commit();
HibernateUtil.getSession().close();
}
Listagem 11: exceção lançada pela validação do Hibernate
Exception in thread "main" org.hibernate.PropertyValueException: not-null property references a null or transient value: org.hibernate.validator.test.hibernateIntegracao.Linha.codigo
...
Listagem 12: exceção lançada pela validação do Hibernate
Exception in thread "main" org.hibernate.validator.InvalidStateException: validation failed for: org.hibernate.validator.test.hibernateIntegracao.Linha
...
Camada de apresentaçãoJBoss Seam é um poderoso framework para a construção de aplicações Web 2.0, unificando e integrando tecnologias como: AJAX, JSF, EJB3, Java Portlets e BPM. Este projeto é desenhado para eliminar a complexidade destas arquiteturas, de maneira a possibilitar os desenvolvedores criar aplicações web simplesmente anotando classes POJOs [7].
JavaServer Faces é um framework MVC, também para desenvolvimento de aplicações Web, que permite o desenvolvimento de aplicações para internet ao estilo Delphi, (arrastando e soltando). Considerado por muitos, a última palavra em desenvolvimento Web, resultado obtido pela maturidade de tecnologias como JSP/Servlet (Model1), Model2 (MVC) e Struts.
Trabalhando juntas, as tecnologias JSF e JBoss SeamÔ pode-se disparar o processo de validação até a camada de apresentação usando as tags
e , desta forma, enquanto as regras são expressas no model as mensagens de erro são apresentadas na view, sem duplicação para o usuário, um exemplo de utilização desta tag encontra-se na listagem 13, veja também documentação do JBoss SeamÔ para maiores detalhes.
Listagem 13: visualizando as mensagens de erro com JSF
< s:validateAll>
< div>
Telefone:
< h:inputText value="#{validator.phone}" required="true" />
< /div>
< /s:validateAll>
JSR 303: Bean Validation
Bean Validation, já aprovada e prevista para a versão 7 do Java, deverá padronizar a definição de validadores, regras de validação e afins. Baseado em annotations e descritores XML, este novo recurso fornecerá em tempo de execução meios de checar e validar as propriedades de classes persistentes. Para validações mais comuns e simples, serão utilizadas annotations, enquanto casos mais complexos ou regras de negócios específicas utilizarão um descritor XML para configurar a validação [1].
Fortemente inspirada no Commons-Validator, Hibernate Validator e XWork Validation Framework, a JSR 303 poderá ser utilizada não somente em aplicações web, mas também em aplicações desktop e para a camada de persistência, permitirá o envio mensagens de erros internacionalizadas, e também a personalização através da classe ResourceBundle.
O líder deste sub-projeto (Hibernate Validator) é um dos membros da JSR 303, por isto podemos esperar que esta nova especificação do Java seja bem semelhante ao que descrevemos neste artigo.
Considerações Finais
Consistência de dados, testes e validação, são itens dos quais todos precisamos, mais cedo ou mais tarde a esta necessidade “bate nossa porta”, é nesta hora que precisamos tomar a decisão de qual tecnologia e estratégia adotar. O Hibernate Validator fornece um tratamento bem interessante, como ela é fortemente ligada a Bean Validation, acreditamos que vale, e muito, dedicar algumas horas para a pesquisa e aprendizado deste recurso, não somente porque é algo que está por vir no Java 7, mas porque este tende a ser um padrão no desenvolvimento de aplicações.
Para facilitar a vida do leitor, está disponível para download um projeto eclipse com os fontes utilizados para validar as informações contidas neste material. Nele você encontrara exemplos de validações, incluindo até mesmo algumas que não foram extensivamente exploradas neste texto. É importante ressaltar que muitos dos códigos foram retirados ou modificados dos próprios testes unitários (JUnit) do Hibernate Validator, por isto, caso o leitor queira ter acesso aos originais basta buscar direto na fonte.
Referências
[1] JSR 303: Bean Validation http://jcp.org/en/jsr/detail?id=303
[2] Hibernate - http://www.hibernate.org
[3] Commons-Validator - http://jakarta.apache.org/commons/validator
[4] XWork Validation Framework - http://wiki.opensymphony.com/display/XW/Validation+Framework
[5] API da classe ResourceBundle
[6] Internacionalization support - http://java.sun.com/javase/6/docs/technotes/guides/intl/index.html
[7] http://www.jboss.com/products/seam