Hibernate Validator et Spring: Utiliser Javascript dans Java (JSR 223)

Hibernate Validator (HV) est l'implémentation de référence de la JSR 303.

La JSR 223 permet de réutiliser les langages de scipt dans le monde java comme nous le montrons ci-après avec HV 4.2.

La version HV 4.2 apporte de nouvelles fonctions simplifiant la vie du développeur.

Ces améliorations répondent à des besoins concrets et n'exigent pas d'écrire du code.

Quelques annotations suffiront.

La démo ci-après détaille quelques notions avancées de HV 4.2.

Elle permet de valider deux propriétés inter-dépendantes, par exemple:

- Vérifier que deux propriétés (mail, motdepasse, ...) sont identiques.

- Comparer une propriété à une autre.

Ces différentes validations se feront via un language de script.

Ces différentes validations se feront via un language de script ( javascript par défaut).

En effet, l'annotation @ScriptAssert de HV est utilisée.

Cette annotation s'appuie sur le JSR 223 ("Scripting for the Java" ou encore comment utiliser les langages de script (javascript par exemple) dans le monde java.

Noter que d'autres annotations de HV (non JSR 303) sont aussi pratiquées dans ce démo comme par exemple @Email.

La démo est un projet java SE standard testé sous java7 (valable pour java 6).

Par contre pour java 5 une configuration supplémentaire est nécessaire (voir annexe).

ETAPE 1. Configurer les dépendences

Ajouter dans le pom.xml la dépendence :

<!-- Hibernate Validator -->
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-validator</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
		</exclusion>
	</exclusions>
	<version>4.2.0.Final</version>
</dependency>

Et les dépendances suivantes sont utiles à notre test unitaire:

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.10</version>
	<scope>tets</scope>
</dependency>
<!-- spring-test -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
	<version>3.1.2.RELEASE</version>
	<scope>test</scope>
</dependency>

ETAPE 2. Ecrire un POJO avec les contraintes (annotations)

La classe PersonForm ci-après comporte un nombre important de contraintes pour illustration.

package fr.netapsys.validavancee;
import java.io.Serializable;
import java.util.Date;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.ScriptAssert;

@ScriptAssert.List({
 @ScriptAssert(lang="javascript",script="_this.dateCreation.before(_this.dateMaj)",message="Deux dates incompatibles!"),
 @ScriptAssert(lang="javascript",script=" _this.mail2.equals(_this.mail1)",  message="Mails non identiques!")
})
public class PersonForm implements Serializable{
	 
	@Size(min=3,max=10,message="La taille doit etre entre 3 et 10 car.")
	private String name;
	@Email
	private String mail1, mail2;
	
	private Date dateCreation;

	private Date dateMaj; 
	 
......... omis les setters/getters

Explications:

L'annotation @ScriptAssert apposée sur la classe s'appuie sur la JSR 223 qui permet d'exploiter
les langages de script dans le monde java (voir annexe).

L'annotation @ScriptAssert.List permet de regouper deux annotations de type @ScriptAssert.

La première annotation @ScriptAssert concerne les deux propriétés "dateCreation" et "dateMaj".

Elle précise le type de language de script, ici javascript, suivi des fonctions de validation à effectuer.

La première annotation de la liste permet de valider la contrainte sur deux dates:
-l'attribut "dateCreation" doit être antérieur à la date de mise à jour "dateMaj".

La seconde annotation @ScriptAssert porte sur les deux attributs "mail1" et "mail2":
Elle vérifie la contriante sur ces deux attributs qu'ils ont des valeurs identiques.

ETAPE 3. Configurer Spring

Le fichier de spring nommé spring.xml doit déclarer cette ligne:

   
<bean id="validator"
 class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>    

Pour notre démo le fichier spring ne contient qu'une seule ligne.

ETAPE 4. Ecrire un test unitaire

package fr.netapsys.validavancee;
import static org.junit.Assert.*;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import fr.netapsys.validavancee.PersonForm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring.xml"})
public class TestValidation {
@Autowired Validator validator;
Logger logger = LoggerFactory.getLogger(TestValidation.class);

@Test public void testNameShort() {
	PersonForm pf = new PersonForm();
	pf.setName("TO");
	Utiles.populateDates(pf);
	pf.setMail1("m1@m.m");
	pf.setMail2(pf.getMail1());
	assert (null != validator);
	Set<ConstraintViolation<PersonForm>> 
	   listeErrs = validator.validate(pf);
	//displayViolations(pf);
	assertTrue(listeErrs.size() == 1);
}
@Test public void testNameAndDateIncorrects() {
	PersonForm pf = new PersonForm();
	
	pf.setName("TO"); // incorrect
	Utiles.populateInvalidDates(pf);// incorrect
	pf.setMail1("m1@m.m"); // ok
	pf.setMail2(pf.getMail1());// ok
	Set<ConstraintViolation<PersonForm>> 
	  listeErrs =validator.validate(pf);
	//displayViolations(pf);
	assertTrue(listeErrs.size() == 2);
	
}
@Test public void testMailsNonIdentiq() {
	PersonForm pf = new PersonForm();
	pf.setName("TOTO");// ok
	Utiles.populateDates(pf);// ok
	pf.setMail1("m1mail.fr");// ok
	pf.setMail2("m2@mail.fr"); //incorrect
	Set<ConstraintViolation<PersonForm>> 
	     listeErrs = validator.validate(pf);
	//displayViolations(pf);
	assertTrue(listeErrs.size() == 2);
}
/****** private method********/
private void displayViolations(PersonForm pf){
	Set<ConstraintViolation<PersonForm>> listeErrs= validator.validate(pf);
	if(!listeErrs.isEmpty()){
		final StringBuffer sb=new StringBuffer();
		for( ConstraintViolation<PersonForm> cv:listeErrs){
			sb.append(" '"+cv.getInvalidValue()+ "' " +cv.getMessage()+"\n");
		}
		logger.info("Object "+pf+ " is NOT valid. Details:\n"+sb.toString());
	}
}

}

Explications:

Les tests sont commentés (et correctement nommés) pour faciliter la lecture.


Le test JUnit est configuré pour être lancé par SpringJUnit4ClassRunner comme le mentionne l'annotation @RunWith.

L'annotation @ContextConfiguration permet de charger le contexte de spring basé sur le fichier "applicationContext.xml".

L'annotation @Autowired injecte automatiquement l'unique bean déclaré dans spring nommé validator.

Et les méthodes annotées avec @Test utilisent ce bean pour valider les instances de PersonForm.

La première méthode testNameShort construit une instance de PersonForm avec un nom court.

Et le validator signale cette violation captée dans l'assertion de Junit.

La méthode testNameAndDateIncorrects crée une instance avec un nom court et deux dates incompatibles:

- la date de création est postérieure à celle de mise à jour.

L'assertion JUnit intercepte les 2 violations.

Enfin, la méthode testMailsNonIdentiq teste le format du mail et que les deux mails ne sont pas identiques.

Pour être complet, voici la classe Utiles appelée dans les tests:

package fr.netapsys.validavancee;
import java.util.Calendar;
public class Utiles {
public static void populateDates(PersonForm pf) {
	Calendar calendar = Calendar.getInstance();
	calendar.set(1971, 10, 24);
	pf.setDateCreation(calendar.getTime());
	calendar.set(1971, 10, 25);
	pf.setDateMaj(calendar.getTime());
}	
public static void populateInvalidDates(PersonForm pf) {
	Calendar calendar = Calendar.getInstance();
	calendar.set(1962, 10, 27);
	pf.setDateCreation(calendar.getTime());
	//dateMaj not after dateCreation
	calendar.set(1962, 10, 25);
	pf.setDateMaj(calendar.getTime());
}
}

ETAPE 5. Exécuter les tests

La commande maven :

mvn test

permet de dérouler les tests.

Pour finir, optionnellement vous pouvez décommenter dans les tests les lignes //displayViolations pour afficher les contraintes non respectées.

Penser à l'ajout des dépendences de SLF4j et Log4j (Voir annexe).

ANNEXE

Annexe 1.

Note sur la version de java et la JSR 223:
For evaluation of expressions the Java Scripting API as defined by JSR 223 ("Scripting for the JavaTM Platform") is used.
Therefore an implementation of that API must part of the class path.
This is automatically the case when running on Java 6.
For older Java versions, the JSR 223 RI can be added manually to the class path.
The expressions to be evaluated can be written in any scripting or expression language, for which a JSR 223 compatible engine can be found in the class path.

Annexe2:

Voici l'ensemble des dépendences du pom.xml nécessaires pour ce billet:

<dependencies>
	<!-- Hibernate Validator -->
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-validator</artifactId>
		<exclusions>
			<exclusion>
				<groupId>commons-logging</groupId>
				<artifactId>commons-logging</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-api</artifactId>
			</exclusion>
		</exclusions>
		<version>4.2.0.Final</version>
	</dependency>

	<!-- logging -->
	<dependency>
		<groupId>commons-logging</groupId>
		<artifactId>commons-logging</artifactId>
		<version>1.1</version>
		<scope>runtime</scope>
		<exclusions>
			<exclusion>
				<groupId>avalon-framework</groupId>
				<artifactId>avalon-framework</artifactId>
			</exclusion>
			<exclusion>
				<groupId>logkit</groupId>
				<artifactId>logkit</artifactId>
			</exclusion>
			<exclusion>
				<groupId>javax.servlet</groupId>
				<artifactId>servlet-api</artifactId>
			</exclusion>
			
		</exclusions>
	</dependency>

	<!-- log4j -->
	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>${log4j-version}</version>
		<scope>${dependency-deploiement-scope-runtime}</scope>
		<exclusions>
			<exclusion>
				<groupId>javax.jms</groupId>
				<artifactId>jms</artifactId>
			</exclusion>
			<exclusion>
				<groupId>javax.mail</groupId>
				<artifactId>mail</artifactId>
			</exclusion>
			<exclusion>
				<groupId>com.sun.jmx</groupId>
				<artifactId>jmxri</artifactId>
			</exclusion>
			<exclusion>
				<groupId>com.sun.jdmk</groupId>
				<artifactId>jmxtools</artifactId>
			</exclusion>
		</exclusions>

	</dependency>
	<!-- slf4j -->
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>${slf4j-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-log4j12</artifactId>
		<version>${slf4j-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>log4j</groupId>
				<artifactId>log4j</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.slf4j</groupId>
				<artifactId>slf4j-api</artifactId>
			</exclusion>
		</exclusions>
	</dependency>

	
	<!-- spring-core -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-core</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-asm</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	
	<!-- spring-expression -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-expression</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-core</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	
	<!-- spring-asm -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-asm</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
	</dependency>
	<!-- spring-context -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>commons-logging</groupId>
				<artifactId>commons-logging</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-aop</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-beans</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-core</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-expression</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-asm</artifactId>
			</exclusion>
		</exclusions>
	</dependency>

	<!--  spring-beans -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-beans</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
	</dependency>
	<!-- spring-context -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-aop</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-beans</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-core</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-expression</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-asm</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	<!-- spring-context-support -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-context-support</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope}</scope>
		<exclusions>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-beans</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-core</artifactId>
			</exclusion>
			<exclusion>
				<groupId>org.springframework</groupId>
				<artifactId>spring-context</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	<!-- junit -->
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>${junit-version}</version>
		<exclusions>
		 <exclusion>
		   <groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-core</artifactId>
		 </exclusion>
		</exclusions>

		<scope>${dependency-deploiement-scope-test}</scope>
	</dependency>
	<!-- spring-test -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>${spring-version}</version>
		<scope>${dependency-deploiement-scope-test}</scope>
	</dependency>
</dependencies>
<properties>

	<jdk-version>1.7</jdk-version>
	<projet-encoding>UTF-8</projet-encoding>
	<projet-locale>fr_FR</projet-locale>

	<!-- Type de deploiements -->
	<dependency-deploiement-scope>compile</dependency-deploiement-scope>
	<dependency-deploiement-scope-provided>provided</dependency-deploiement-scope-provided>
	<dependency-deploiement-scope-test>test</dependency-deploiement-scope-test>
	<dependency-deploiement-scope-runtime>runtime</dependency-deploiement-scope-runtime>

	<!-- Versions Dependances -->
	<commons-beanutils-version>1.8.3</commons-beanutils-version>
	<commons-codec-version>1.6</commons-codec-version>
	<commons-collections-version>3.2.1</commons-collections-version>
	<commons-lang-version>2.6</commons-lang-version>
	<commons-logging-version>1.1.1</commons-logging-version>
	<commons-digester-version>2.1</commons-digester-version>
	<javassist-version>3.12.1.GA</javassist-version>
	<cglib-version>2.2.2</cglib-version>

	<!-- Log -->
	<log4j-version>1.2.17</log4j-version>
	<slf4j-version>1.6.6</slf4j-version>

	<!-- Spring -->
	<spring-version>3.1.2.RELEASE</spring-version>

	<!-- Versions Dependances de test -->
	<junit-version>4.10</junit-version>

	<!-- Versions plugins maven -->
	<maven-eclipse-plugin-version>2.9</maven-eclipse-plugin-version>
	<maven-compiler-plugin-version>2.5.1</maven-compiler-plugin-version>
	<maven-resources-plugin-version>2.5</maven-resources-plugin-version>
</properties>	

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *