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;

@Constraint(validatedBy = { org.lucaster.validator.UniqueKeyValidator.class })
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueKey {

	String property();

	String message() default "Value is not unique";

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

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

	@Target({ ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@interface List {
		UniqueKey[] value();
	}

}

Validator

package org.lucaster.validator;

import static de.hashcode.validation.ReflectionUtils.getIdField;
import static de.hashcode.validation.ReflectionUtils.getPropertyValue;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.List;

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;

public class UniqueKeyValidator implements
		ConstraintValidator<UniqueKey, Serializable> {

	@Inject private EntityManager entityManager;

	private UniqueKey constraintAnnotation;

	public UniqueKeyValidator() {}

	public UniqueKeyValidator(final EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	public EntityManager getEntityManager() {
		return entityManager;
	}

	@Override
	public void initialize(final UniqueKey constraintAnnotation) {
		this.constraintAnnotation = constraintAnnotation;
	}

	@Override
	public boolean isValid(final Serializable target,
			final ConstraintValidatorContext context) {

		if (entityManager == null) {
			// eclipselink may be configured with a BeanValidationListener that
			// validates an entity on prePersist
			// In this case we don't want to and we cannot check anything (the
			// entityManager is not set)
			//
			// Alternatively, you can disalbe bean validation during jpa
			// operations
			// by adding the property "javax.persistence.validation.mode" with
			// value "NONE" to persistence.xml
			return true;
		}

		final Class<?> entityClass = target.getClass();

		final CriteriaBuilder criteriaBuilder = entityManager
				.getCriteriaBuilder();

		final CriteriaQuery<Object> criteriaQuery = criteriaBuilder
				.createQuery();

		final Root<?> root = criteriaQuery.from(entityClass);

		try {
			final Object propertyValue = getPropertyValue(target,
					constraintAnnotation.property());
			final Predicate uniquePropertyPredicate = criteriaBuilder.equal(
					root.get(constraintAnnotation.property()), propertyValue);

			final Field idField = getIdField(entityClass);
			final String idProperty = idField.getName();
			final Object idValue = getPropertyValue(target, idProperty);

			if (idValue != null) {
				final Predicate idNotEqualsPredicate = criteriaBuilder
						.notEqual(root.get(idProperty), 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 @UniqueKey '"
							+ constraintAnnotation.property()
							+ "' 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
					.property());
			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;

@Entity
@UniqueKey(property = "title")
// @UniqueKey.List(value = { @UniqueKey(property = { "title" }), @UniqueKey(property = { "author" }) }) // more than one unique keys
public class Book extends Model {

	private static final long serialVersionUID = 8519973666976987438L;

	@NotEmpty @Column(unique = true) 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 – version 1

public boolean persist() {

	try {

		instance = entityManager.merge(instance);
		entityManager.flush();
		id = instance.getId();
		return true;

	} catch (ConstraintViolationException e) {

		e.printStackTrace();

		Set<Book> violations = e.getConstraintViolations();

		for (Object violationObject : violations.toArray()) {
			ConstraintViolation violation = (ConstraintViolation) violationObject;
			FacesUtil.error("bookForm-" + violation.getPropertyPath().toString(), violation.getMessage(), violation.getMessage());
		}

		return false;
	}
}

Usage in controller – version 2

@Inject private javax.validation.Validator validator;

public boolean persist() {

	Set<Book> violations = validator.validate(instance);

	if (!violations.isEmpty()) {
		for (Object violationObject : violations.toArray()) {
			ConstraintViolation violation = (ConstraintViolation) violationObject;
			FacesUtil.error("bookForm-" + violation.getPropertyPath().toString(), violation.getMessage(), violation.getMessage());
		}
		return false;
	}

	else {
		instance = entityManager.merge(instance);
		entityManager.flush();
		id = instance.getId();
		return true;
	}
}

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
Remember to use THIS version of the UniqueKey class, otherwise you will end up using the one from the Maven dependency and everything will break!
This approach only lets you define single column unique constraints.
Constraint violations are detected when the EntityManager flushes, during INVOKE_APPLICATION phase, instead of as part of PROCESS_VALIDATIONS phase.
Constraints violations must be handed programmatically to display messages near the interested input fields.
You need to establish a convention between input fields ids and property names. If you change the input fields ids, you also have to change the code.
Usage in controller – version 2: in this version you check for constraints before persisting, preventing the exception from happening. However, if your entity bean has some @NotNull properties that are set in the @PrePersist or @PreUpdate, you’ll get constraint violations for those fields.
Usage in controller – version 1: this version is more robust, considering the above statement, because the only constraint violation you’ll get will be most probably the unique constraint violation you expect.

 

Leave a Reply