Unique Validator with JSF2 and JPA2 but without Bean Validation

Such a classic! One of your form's fields must be unique in the database.

As we use JPA2 and bean validation, our fist thought was let's use bean validation. But soon, we realized this is not as easy as it should be:

Hopefully, your devoted team has found a solution and is sharing it here with you (and in the projects generated by springfuse)

Instead of annotating an entity property, we leverage the <f:validateBean> tag directly in the view.

Here is an usage example:

<app:input id="username" value="#{account.username}" size="100" label="${msg.account_username}" required="true">
    <f:validateBean binding="#{jpaUniqueValidator}" entity="${account}" property="username" />
</app:input>

When an already existing username is entered, the validator fails and a nice error message is displayed.

Source code

Here is the jpaUniqueValidator component source code:

@FacesValidator(value = "jpaUniqueValidator")
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class JpaUniqueValidator implements Validator {

    @Inject
    private JpaUniqueSupport jpaUniqueSupport;
    @Inject
    private ResourcesUtil resourcesUtil;

    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        if (entity == null && isBlank(property)) {
            return;
        }

        if (entity == null) {
            throw new IllegalStateException("Missing 'entity' attribute");
        }

        if (property == null) {
            throw new IllegalStateException("Missing 'property' attribute");
        }

        if (!jpaUniqueSupport.isUnique(entity, property, value)) {
            FacesMessage fm = new FacesMessage(resourcesUtil.getProperty(entity.getClass().getSimpleName().toLowerCase() + "_" + property + "_already_exists"));
            fm.setSeverity(SEVERITY_ERROR);
            throw new ValidatorException(fm);
        }
    }

    private Identifiable<?> entity;
    private String property;

    public void setEntity(Identifiable<?> entity) {
        this.entity = entity;
    }

    public Identifiable<?> getEntity() {
        return entity;
    }

    public void setProperty(String property) {
        this.property = property;
    }

    public String getProperty() {
        return property;
    }
}

It invokes the jpaUniqueSupport.isUnique(entity, property, value) method which is listed below:

@Named
@Singleton
public class JpaUniqueSupport {
    @PersistenceContext
    private EntityManager entityManager;

    public boolean isUnique(Identifiable<?> entity, String propertyName, Object propertyValue) {
        if (entity == null || propertyValue == null) {
            return true;
        }
        checkJpaEntityPresence(entity);
        return !existsInDatabase(getEntityName(entity), entity, propertyName, propertyValue);
    }

    private boolean existsInDatabase(String entityName, Identifiable<?> entity, String propertyName, Object propertyValue) {
        if (entity.isIdSet()) {
            return existsInDatabaseOnOtherObjects(entityName, entity, propertyName, propertyValue);
        } else {
            return existsInDatabaseOnAllObjects(entityName, entity, propertyName, propertyValue);
        }
    }

    private boolean existsInDatabaseOnAllObjects(String entityName, Identifiable<?> entity, String propertyName, Object propertyValue) {
        return entityManager //
                .createQuery( //
                        format("select count(c) from %s c where %s = :propertyValue", entityName, propertyName), Long.class) //
                .setParameter("propertyValue", propertyValue) //
                .getSingleResult() > 0;
    }

    private boolean existsInDatabaseOnOtherObjects(String entityName, Identifiable<?> entity, String propertyName, Object propertyValue) {
        return entityManager //
                .createQuery( //
                        format("select count(c) from %s c where %s = :propertyValue and %s != :id", //
                                entityName, propertyName, getPkAttributeName(entity.getClass())), Long.class) //
                .setParameter("propertyValue", propertyValue) //
                .setParameter("id", entity.getId()) //
                .getSingleResult() > 0;
    }

    private void checkJpaEntityPresence(Identifiable<?> value) {
        Entity entity = findAnnotation(value.getClass(), Entity.class);
        if (entity == null) {
            throw new IllegalStateException(value.getClass().getSimpleName() + " is not a JPA entity");
        }
    }

    private String getEntityName(Identifiable<?> value) {
        Entity entity = findAnnotation(value.getClass(), Entity.class);
        if (isBlank(entity.name())) {
            return value.getClass().getSimpleName();
        }
        return entity.name();
    }

    private String getPkAttributeName(Class<?> clazz) {
        String pkAttributeName = getPkAttributeName(clazz, Id.class);
        return pkAttributeName != null ? pkAttributeName : getPkAttributeName(EmbeddedId.class);
    }

    @SuppressWarnings( { "rawtypes", "unchecked" })
    private String getPkAttributeName(Class<?> clazz, Class annotation) {
        for (Method m : clazz.getMethods()) {
            if (findAnnotation(m, annotation) != null) {
                return methodToProperty(m);
            }
        }
        for (Field f : clazz.getFields()) {
            if (f.getAnnotation(annotation) != null) {
                return f.getName();
            }
        }
        return null;
    }

    private String methodToProperty(Method m) {
        String value = m.getName();
        if (value.startsWith("get")) {
            int length = "get".length();
            return value.substring(length, length + 1).toLowerCase() + value.substring(length + 1);
        } else if (value.startsWith("is")) {
            int length = "is".length();
            return value.substring(length, length + 1).toLowerCase() + value.substring(length + 1);
        } else {
            return value;
        }
    }
}

Enjoy!

The SpringFuse/Jaxio team.


comments powered by Disqus