You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.
When you want to make sure that a field or a property meets a general rule, let's say if it fits a given range, or meets a predefined format - you will use Hibernate field/property validators. That's what the well known @Max, @Min, @Length, @NotNull and other org.hibernate.validator.* annotations do. They are called before the entity is persisted, so if a rule is not met, Hibernate throws an exception. But Seam makes even a better use of these rules, by calling them automatically immediately after a view has been submitted (this is achieved by <s:validate/> and <s:validateAll/> tags). If something is wrong, the last view is redisplayed with a detailed error message attached to each problematic field, without contacting the database.
But what to do when you have to validate relations between more fields of the same entity? Let's say, when you have to check that a start date is before the end date? That's when you use a class-level validator. Unlike property validators described above, these are not called by the Seam after the page submit, but only by the Hibernate - and it will throw an ugly exception when a rule is not met! That is why you have to call these validators manually from EntityHome before a persist() or update(). Refer to http://seamframework.org/Community/EntityLevelValidation for more details. I guess, that's something we can live with, until such a feature is provided automatically by Seam in a future release.
Finally, here we come to the biggest issue - how to elegantly validate a uniqueness constraint? Unlike the class-level validator, this problem requires a validation on a set level, i.e. requires you to have access to the entity manager. In the architecture where an entity is blissfully unaware of the persistence context, that's a big no-no! Therefore, it has to be validated from the EntityHome, that does have a reference to the EntityManger. Some people would naively say that you could catch a possible exception around persist() or update() and then return a user to the last view, with a message explaining that a constraint has been violated - but that's even a bigger no-no. Firstly, an exception would break the transaction, and secondly - using exceptions for handling expected events is time consuming and generally a bad practice.
What you should to is to make a query searching for other entities with the same properties, and if one is found, just display a message without making an INSERT or UPDATE. Otherwise, you can put it in the database. There is still a possibility that in the meantime another entity that collides with that entity was inserted by another user, but that indeed is an exception, and should be handled as one (pages.xml is a good place to deal with it).
That operation requires creating entity-specific queries and you can manually hard-code one for each entity requiring checks for unique constraints... Or, you could make a bet an extension of the EntityHome class, that checks its entity for annotations that you will use to define unique constraints on the Entity level.
Let's write that annotation:
package com.jonniezg.seam; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Defines a list of fields that are unique for an entity. * * @see ExtendedEntityHome */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Unique { /** * A list of field (property) names. */ String[] value(); }
Now, let's write our extension of EntityHome and call it ExtendedEntityHome. It overrides persist() and update() methods, checking the entity class for:
If the validation fails, a message will be issued and the action will return string constraintViolated
.
Here it is:
package com.jonniezg.seam; import static org.jboss.seam.international.StatusMessage.Severity.ERROR; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.persistence.Query; import org.jboss.seam.annotations.Transactional; import org.jboss.seam.core.Expressions; import org.jboss.seam.core.Expressions.ValueExpression; import org.jboss.seam.framework.EntityHome; import org.jboss.seam.framework.Home; /** * This class extends the EntityHome with uniqueness checking facilities. It's a good place to put your own enhancements * as well. * * It checks the entity class for: * <ul> * <li>An identifier field annotated with {@link javax.persistence.Id}</li> * <li>A {@link Unique} annotation on the class level, containing a list of unique fields</li> * </ul> * * If the validation fails, a message will be issued and the action will return string "constraintViolated". * * @author Arsen Torbarina * * @param <E> * Entity class */ public class ExtendedEntityHome<E> extends EntityHome<E> { private String idField; private String queryCreate; private String queryUpdate; private List<String> uniqueFields = new ArrayList<String>(0); private List<Method> getters = new ArrayList<Method>(0); @SuppressWarnings("unchecked") private ValueExpression constraintViolationMessage; private boolean built = false; /** * Copies names of unique fields specified by the {@link Unique} into a string list. */ private void findUniqueFields() { for (Annotation a : getEntityClass().getAnnotations()) { if (a instanceof Unique) { uniqueFields = Arrays.asList(((Unique) a).value()); break; } } } /** * Finds a field annotated with @Id tag. */ private void findIdField() { OUTER: for (Method m : getEntityClass().getMethods()) { for (Annotation a : m.getAnnotations()) { if (a instanceof javax.persistence.Id) { idField = m.getName(); break OUTER; } } } idField = fieldName(idField); } /** * Builds EJBQLs for validation before update and before create. */ protected void buildQueries() { StringBuilder sb = new StringBuilder("select o from ").append(getSimpleEntityName()).append(" o where "); String delim = ""; for (String field : uniqueFields) { sb.append(delim); sb.append("o.").append(field).append("="); sb.append(":").append(field); delim = " and "; } queryCreate = sb.toString(); sb.append(delim).append("o.").append(idField).append("!=:id"); queryUpdate = sb.toString(); } /** * Finds getters for constrained fields. */ protected void findGetters() { getters.clear(); for (String field : uniqueFields) { String s = firstUpper(field); try { Method m; try { m = getEntityClass().getDeclaredMethod("get" + s); } catch (NoSuchMethodException e) { m = getEntityClass().getDeclaredMethod("is" + s); } getters.add(m); } catch (Exception e) { throw new RuntimeException(e); } } } /** * Gets the name of the entity identifier field. * * @return field name */ public String getIdField() { return idField; } /** * Sets the name of the entity identifier field. Can be used as an alternative to the {@link javax.persistence.Id} * annotation. * * @param idField * field name */ public void setIdField(String idField) { this.idField = idField; } /** * Gets a list of field names that together form a unique constraint. * * @return a list of field names */ public List<String> getUniqueConstraints() { return uniqueFields; } /** * Sets a list of fields that together form a unique constraint. Can be used as an alternative to the * {@link Unique} annotation. * * @param uniqueFields */ public void setUniqueFields(List<String> uniqueFields) { this.uniqueFields = uniqueFields; built = false; } /** * Check if the uniqueness constraint is respected, and if so, persist unmanaged entity instance to the underlying * database. If the persist is successful, a log message is printed, a {@link javax.faces.application.FacesMessage } * is added and a transaction success event raised.<br/> * If the uniqueness constraint is violated, an error status message is issued and the "constraintViolated" string * is returned. * * @see Home#createdMessage() * @see Home#raiseAfterTransactionSuccessEvent() * * @return "persisted" if the persist is successful, "constraintViolated" if the constraint is violated */ @Override @Transactional public String persist() { return persistOrUpdate(false); } /** * Check if the uniqueness constraint is respected and flush any changes made to the managed entity instance to the * underlying database. <br /> * If the update is successful, a log message is printed, a {@link javax.faces.application.FacesMessage} is added * and a transaction success event raised.<br/> * If the uniqueness constraint is violated, an error status message is issued and the "constraintViolated" string * is returned. * * @see Home#updatedMessage() * @see Home#raiseAfterTransactionSuccessEvent() * * @return "persisted" if the update is successful, "constraintViolated" if the constraint is violated */ @Override @Transactional public String update() { return persistOrUpdate(true); } private String persistOrUpdate(boolean update) { if (!built) { findUniqueFields(); findIdField(); findGetters(); buildQueries(); built = true; } Query q = getEntityManager().createQuery(update ? queryUpdate : queryCreate); for (int i = 0; i < uniqueFields.size(); i++) { Method getter = getters.get(i); String field = uniqueFields.get(i); Object val; try { val = getter.invoke(getInstance()); } catch (Exception e) { throw new RuntimeException(e); } q.setParameter(field, val); } if (update) { q.setParameter("id", getId()); } if (q.getResultList().size() > 0) { getStatusMessages().addFromResourceBundleOrDefault(ERROR, getConstraintViolationKey(), getConstraintViolationMessage().getExpressionString()); if (update) { getEntityManager().refresh(getInstance()); } return "constraintViolated"; } if (update) { return super.update(); } else { return super.persist(); } } @Override protected void initDefaultMessages() { super.initDefaultMessages(); Expressions expressions = new Expressions(); if (constraintViolationMessage == null) { constraintViolationMessage = expressions.createValueExpression("Constraint violation"); } } /** * Gets constraint violation message key for the resource bundle. * * @return */ protected String getConstraintViolationKey() { return getMessageKeyPrefix() + "constraintViolation"; } /** * Gets constraint violation message. * * @return */ @SuppressWarnings("unchecked") public ValueExpression getConstraintViolationMessage() { return constraintViolationMessage; } /* * ---- Utilities ----------- */ private static String firstLower(String str) { if (str.length() == 1) { return str.toLowerCase(); } return str.substring(0, 1).toLowerCase() + str.substring(1); } private static String firstUpper(String str) { if (str.length() == 1) { return str.toUpperCase(); } return str.substring(0, 1).toUpperCase() + str.substring(1); } private static String fieldName(String str) { if (str == null) return null; if (str.startsWith("get")) { str = str.substring(3); } else if (str.startsWith("is")) { str = str.substring(2); } return firstLower(str); } }
Now when you have these two classes, it is time to use them in an example. The only specific thing in the Entity class is the @Unique annotation containing the list of properties that form a unique constraint. Some may say that we could have used the database columns instead, defined by the @UniqueConstraint annotation. That would require some more work converting DB column names back to the property names, and you are welcome to write it if you feel like fiddling with it. (NB: That is not an easy task, because the Hibernate NamingStrategy converts properties to column names and not the other way around.)
package com.jonniezg.examples.model; import com.jonniezg.seam.Unique; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.hibernate.validator.Length; @Entity @Table(uniqueConstraints = @UniqueConstraint(columnNames = { "first_name", "last_name" }), name = "person") @Unique( { "firstName", "lastName" }) public class Person implements Serializable { private Long id; private String firstName; private String lastName; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Length(max = 50) public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Length(max = 50) public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
After preparing our Entity class by adding the proper annotation, we just have to pimp up the original Seam-gen generated PersonHome class by replacing the EntityManager with our ExtendedEntityManager:
package com.jonniezg.inventory.action; .... @Name("personHome") public class ResourceTypeHome extends ExtendedEntityHome<ResourceType> { .... }
And that's it! If you want a custom uniqueness-violation message defined per class, you can add an entry to the messages_??.properties:
Person_constraintViolation=A person with the same name already exists
If you don't like the keys being formed this way, you could play around with the getConstraintViolationKey() method, or add a message property to the Unique annotation.
One final note: if your constraints contain properties that reference other entities, you must override hashCode() and equals() methods on these entities, implementing a database identity, that is - check only if the identifiers of the two compared entities of the same type are equal.