Requirements:
JBoss Developer Studio 5
JBoss AS 7.1.1.Final
Seam 3.1.0.Final Validation Module (for dependency injection in validators)
Seam 3.1.0.Final Solder Module (needed by Validation Module)
de.hashcode:jsr303-validators:1.1 (for some utility methods in the validator)

Annotation

package org.lucaster.validator;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { UniqueValidator.class })
public @interface Unique {

	String message() default "Field is not unique!";

	/**
	 * EL expression evaluating to the producer method of the entity bean
	 * instance we are validationg.<br/>
	 * <br/>
	 *
	 * Example: "#{controllerName.getInstance}"
	 *
	 * @return
	 */
	String beanInstanceEL();

	/**
	 * Name of the property/column that is unique
	 *
	 * @return
	 */
	String propertyName();

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

Validator

package org.lucaster.validator;

import static org.lucaster.util.ReflectionUtils.getIdField;
import static org.lucaster.util.ReflectionUtils.getPropertyValue;

import java.util.List;

import javax.faces.validator.FacesValidator;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderDefinedContext;

import org.jboss.solder.el.Expressions;

@FacesValidator("uniqueFieldValidator")
public class UniqueValidator implements ConstraintValidator<Unique, Object> {

	private Unique constraintAnnotation;
	private String beanInstanceEL;
	private String propertyName;

	private Object instance;
	private Class<?> entityClass;
	private String idPropertyName;
	private Object idValue;

	@Inject private EntityManager entityManager;
	@Inject private Expressions expressions;

	@Override
	public void initialize(Unique constraintAnnotation) {
		this.constraintAnnotation = constraintAnnotation;
		beanInstanceEL = constraintAnnotation.beanInstanceEL();
		propertyName = constraintAnnotation.propertyName();
		instance = expressions.evaluateMethodExpression(beanInstanceEL);
		entityClass = instance.getClass();
	}

	@Override
	public boolean isValid(Object value, ConstraintValidatorContext context) {

		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
		Root<?> root = criteriaQuery.from(entityClass);

		Predicate uniquePropertyPredicate = criteriaBuilder.equal(root.get(propertyName), value);

		try {

			idPropertyName = getIdField(entityClass).getName();

			idValue = getPropertyValue(instance, getIdField(entityClass).getName());

			if (idValue != null) {
				Predicate idNotEqualsPredicate = criteriaBuilder.notEqual(root.get(idPropertyName), idValue);
				criteriaQuery.select(root).where(uniquePropertyPredicate, idNotEqualsPredicate);
			} else {
				criteriaQuery.select(root).where(uniquePropertyPredicate);
			}			

		} catch (final Exception e) {
			throw new RuntimeException("An error occurred when trying to create the jpa predicate for the @Unique '" + constraintAnnotation.propertyName() + "' on bean " + entityClass + ".", e);
		}

		final List<Object> resultSet = entityManager.createQuery(criteriaQuery).getResultList();

		if (!resultSet.isEmpty()) {
			ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(constraintAnnotation.message());
			NodeBuilderDefinedContext nbdc = cvb.addNode(constraintAnnotation.propertyName());
			ConstraintValidatorContext cvc = nbdc.addConstraintViolation();
			cvc.disableDefaultConstraintViolation();
			return false;
		}

		return true;

	}

}

Usage in entity bean

package org.lucaster.model;

import javax.persistence.Column;
import javax.persistence.Entity;

import org.hibernate.validator.constraints.NotEmpty;
import org.lucaster.validator.Unique;

@Entity
public class Book extends Model {

	private static final long serialVersionUID = 8519973666976987438L;

	@NotEmpty @Column(unique = true) @Unique(beanInstanceEL = "#{bookHomeCRUD.getInstance}", propertyName = "title") private String title;
	private String author;
	private int pages;

	@Override
	public String toString() {
		return title + ", " + author + ", " + pages;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public int getPages() {
		return pages;
	}

	public void setPages(int pages) {
		this.pages = pages;
	}
}

Usage in controller

@Named("bookHomeCRUD")
@Stateful
@RequestScoped
@AutoValidating
@Logged
public class BookCRUD implements Serializable {
	// ...
	private Book instance;
	// ...
	public Book getInstance() {
		return instance;
	}
	// ...
}

The page

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:p="http://primefaces.org/ui"
	xmlns:s="http://jboss.org/seam/faces"
	xmlns:pm="http://primefaces.org/mobile">

<h:head>
	<title>Book</title>
	<style type="text/css">
		.ui-widget, .ui-widget .ui-widget {
			font-size: 90% !important;
		}
	</style>
</h:head>

<h:body>

	<p:messages globalOnly="false" autoUpdate="true" showDetail="true" showSummary="true"/>

	<p:fieldset legend="Book #{bookHomeCRUD.idDefined ? bookCRUD.id : ''}">
		<h:form id="bookForm">
			<h:panelGrid columns="3" >
				<p:outputLabel for="title" value="Title"/>
				<p:inputText id="title" value="#{bookCRUD.title}" required="true" type="text">

				</p:inputText>
				<p:message for="title" />
				<p:outputLabel for="author" value="Author"/>
				<p:inputText id="author" value="#{bookCRUD.author}" required="true" type="text"/>
				<p:message for="author" />
				<p:outputLabel for="pages" value="Pages"/>
				<p:inputText id="pages" value="#{bookCRUD.pages}" required="true" type="number">
					<f:convertNumber integerOnly="true" maxFractionDigits="0"/>
				</p:inputText>
				<p:message for="pages" />
			</h:panelGrid>
			<p:commandButton value="Save" action="#{bookHomeCRUD.persist}"  rendered="#{!bookHomeCRUD.idDefined}" ajax="false" >

			</p:commandButton>
			<p:commandButton value="Update" action="#{bookHomeCRUD.update}" rendered="#{bookHomeCRUD.idDefined}" ajax="false" >
				<f:param name="id" value="#{bookCRUD.id}"/>
			</p:commandButton>

		</h:form>
		<h:link value="Create New"/>
	</p:fieldset>

	<p:separator />

	<p:dataTable id="bookList" value="#{bookListCRUD}" var="_book">
		<f:facet name="header">Books</f:facet>
		<p:column headerText="Id">#{_book.id}</p:column>
		<p:column headerText="Title">#{_book.title}</p:column>
		<p:column headerText="Author">#{_book.author}</p:column>
		<p:column headerText="Pages">#{_book.pages}</p:column>
		<p:column>
			<h:form>
				<h:link value="Edit" outcome="bookCRUD.xhtml">
					<f:param name="id" value="#{_book.id}" />
				</h:link>
				<p:spacer width="5"/>
				<p:commandLink value="Remove" action="#{bookHomeCRUD.remove}" ajax="false">
					<f:param name="id" value="#{_book.id}" />
				</p:commandLink>
			</h:form>
		</p:column>
	</p:dataTable>

</h:body>
</html>

Remarks
With this approach you finally get to annotate the interested property in the entity class.
Constraint violations are detected during the PROCESS_VALIDATIONS phase, as desired.
There is coupling between the entity class and the bean that saves the entity, which is bad.
If you have more beans that save the entity, you’ll get bad side effects.

 

Leave a Reply