I'm posting my latest version of the Unique Validator just in case it is of any help to anyone else.
Some changes that I made are as follows:
Changing the annotation to apply to a class instead of a field or method. This allows me to reflectively get multiple field values, since the object being passed to the isValid() method is now the whole class instead of just a single field.
Removed the need to specify parameters in favor of parsing the parameters from the specified HQL.
Added a wrapper annotation to support a collection of Unique validators for any particular class.
The code is listed below:
Uniques.java
Code:
package com.docfinity.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Uniques {
Unique[] value();
}
Unique.java
Code:
package com.docfinity.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hibernate.validator.ValidatorClass;
@ValidatorClass(UniqueValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Unique {
/** Query string to execute to determine uniqueness. */
String hql();
/** The message to display if validation fails. */
String message() default "The specified field value is not unique.";
}
UniqueValidator.java
Code:
package com.docfinity.validator;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.validator.Validator;
import com.docfinity.business.SystemException;
/**
* An implementation of the Hibernate Validator interface, used to
* determine the uniqueness of a value. <p>
*
* This validator will get the named parameters from the HQL statement and
* retrieve the values for those parameters from the instance of the class
* being validated.
*/
public class UniqueValidator implements Validator<Unique> {
/** Hibernate session factory for building queries. */
private static SessionFactory sessionFactory;
/** Query string to determine a value's uniqueness. */
private String hql;
/** The collection of named parameters in the HQL statement. */
private String[] params;
/**
* Initializes the validator instance with properties from the
* specified annotation parameters.
*
* @param parameters the parameters of the field to validate's Unique annotation
*/
public void initialize(final Unique parameters) {
this.hql = parameters.hql();
this.params = createParameterList(this.hql);
}
/**
* Extracts the named parameters from the specified HQL statement.
*
* @param query the HQL statement to parse
* @return an array of all the named parameters (of the form :name) found in the provided string
*/
private String[] createParameterList(final String query) {
final Matcher matcher = Pattern.compile(":[^\\s]*").matcher(query);
List<String> paramList = new ArrayList<String>();
while(matcher.find()) {
paramList.add(this.hql.substring(matcher.start() + 1, matcher.end()));
}
return paramList.toArray(new String[paramList.size()]);
}
/**
* Method to determine whether or not the value passes validation. <p>
*
* Validation in this case refers to a value being unique.
*
* @param value the value to validate for uniqueness
* @return true if the value is unique, false otherwise
*/
public boolean isValid(final Object value) {
Query query = sessionFactory.getCurrentSession().createQuery(this.hql);
Class valueClass = value.getClass();
Field field;
for(int i = 0; i < this.params.length; i++) {
try {
field = valueClass.getDeclaredField(this.params[i]);
field.setAccessible(true);
query.setParameter(this.params[i], (null != field.get(value)) ? field.get(value) : "");
} catch(final NoSuchFieldException e) {
throw new SystemException(e.getMessage());
} catch(final IllegalAccessException e) {
throw new SystemException(e.getMessage());
}
}
return query.list().size() == 0;
}
/**
* Sets the Hibernate SessionFactory to use for building queries for validation.
*
* @param sessionFactory a valid SessionFactory instance
*/
public void setSessionFactory(final SessionFactory sessionFactory) {
UniqueValidator.sessionFactory = sessionFactory;
}
}
An example class using the validator is as follows:
Category.java
Code:
package com.docfinity.classification.entity;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;
import com.docfinity.auditing.AuditType;
import com.docfinity.auditing.Auditable;
import com.docfinity.classification.enums.CategoryStatus;
import com.docfinity.entity.Persistable;
import com.docfinity.entity.auditing.AuditMessage;
import com.docfinity.validator.Unique;
/**
* Class to define a Category entity. <p>
*
* Categories are used to classify groups of related documents/images. A category
* can be further broken down into document types, which are a more specific
* grouping of documents/images within a category.
*/
@Entity
@Table(name = "Categories")
@Unique(hql = "FROM Category c WHERE c.name = :name AND c.id != :id", message = "Category name must be unique.")
public class Category implements Persistable, Auditable {
/** A description for the category. */
@Lob
@Column(nullable = true)
@NotNull(message = "Category description cannot be null.")
private String description;
/** The collection of document type classifications within the category. */
@OneToMany(cascade = { CascadeType.ALL }, mappedBy = "category")
@OrderBy(value = "name")
private List<DocumentType> documentTypes;
/** Unique identifier for a category. */
@Id
@GenericGenerator(name = "uuid", strategy = "uuid")
@GeneratedValue(generator = "uuid")
@Column(columnDefinition = "CHAR(32)")
private String id;
/** Unique name for the category. */
@Column(unique = true)
@NotNull(message = "Category name cannot be null.")
@Length(min = 1, max = 75, message = "Category name cannot be less than 1 character and more than 75 characters.")
private String name;
/** Status of the category. */
@Column(length = 20)
@NotNull(message = "Category status cannot be null.")
@Enumerated(EnumType.STRING)
private CategoryStatus status;
/**
* Default constructor. <p>
*
* This constructor should not be called directly.
*/
public Category() {
// Default constructor
}
... rest of code omitted ...
If anyone has any ideas on how to improve this functionality, or if there are any glaring issues, please let me know.