package edu.ualberta.med.biobank.validator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.validation.ConstraintValidatorFactory;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.MessageInterpolator;
import javax.validation.TraversableResolver;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import org.hibernate.EntityMode;
import org.hibernate.FlushMode;
import org.hibernate.action.EntityDeleteAction;
import org.hibernate.cfg.beanvalidation.HibernateTraversableResolver;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.event.AbstractPreDatabaseOperationEvent;
import org.hibernate.event.EventSource;
import org.hibernate.event.PreCollectionRecreateEvent;
import org.hibernate.event.PreCollectionRecreateEventListener;
import org.hibernate.event.PreCollectionRemoveEvent;
import org.hibernate.event.PreCollectionRemoveEventListener;
import org.hibernate.event.PreCollectionUpdateEvent;
import org.hibernate.event.PreCollectionUpdateEventListener;
import org.hibernate.event.PreDeleteEvent;
import org.hibernate.event.PreDeleteEventListener;
import org.hibernate.event.PreInsertEvent;
import org.hibernate.event.PreInsertEventListener;
import org.hibernate.event.PreUpdateEvent;
import org.hibernate.event.PreUpdateEventListener;
import org.hibernate.persister.entity.EntityPersister;
import edu.ualberta.med.biobank.validator.engine.LocalizedConstraintViolation;
import edu.ualberta.med.biobank.validator.group.PreDelete;
import edu.ualberta.med.biobank.validator.group.PreInsert;
import edu.ualberta.med.biobank.validator.group.PreUpdate;
import edu.ualberta.med.biobank.validator.messageinterpolator.OgnlMessageInterpolator;
public class BeanValidationHandler implements PreInsertEventListener,
PreUpdateEventListener, PreDeleteEventListener,
PreCollectionUpdateEventListener, PreCollectionRecreateEventListener,
PreCollectionRemoveEventListener {
private static final long serialVersionUID = 1L;
private final ValidatorFactory factory;
private final MessageInterpolator messageInterpolator =
new OgnlMessageInterpolator();
// TODO: I really hope this doesn't hold onto TONS of objects. Investigate!
private final ConcurrentHashMap<EntityPersister, Set<String>> associationsPerEntityPersister =
new ConcurrentHashMap<EntityPersister, Set<String>>();
public BeanValidationHandler() {
factory = Validation.buildDefaultValidatorFactory();
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
validate(event, new Class<?>[] { PreInsert.class, Default.class });
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
validate(event, new Class<?>[] { PreUpdate.class, Default.class });
return false;
}
@Override
public boolean onPreDelete(PreDeleteEvent event) {
validate(event, new Class<?>[] { PreDelete.class });
return false;
}
@Override
public void onPreUpdateCollection(PreCollectionUpdateEvent event) {
// Object entity = event.getAffectedOwnerOrNull();
// String role = event.getCollection().getRole();
// String propertyName = StringHelper.unqualify(role);
// not sure this is actually important. The owning object will be
// checked first anyways
// System.out.println("Update. Role: " +
// event.getCollection().getRole());
}
@Override
public void onPreRemoveCollection(PreCollectionRemoveEvent event) {
// TODO: this is important when the owning entity is deleted, to check
// this first.
Object entity = event.getAffectedOwnerOrNull();
if (entity == null) return;
EventSource session = event.getSession();
boolean queuedForDeletion = false;
@SuppressWarnings("rawtypes")
ArrayList deletions = session.getActionQueue().cloneDeletions();
for (Object deletion : deletions) {
EntityDeleteAction action = (EntityDeleteAction) deletion;
if (action.getInstance() == entity) {
queuedForDeletion = true;
break;
}
}
if (queuedForDeletion) {
// only validate the owning object for deletion if it is set
// to be deleted in the Hibernate ActionQueue
Class<?>[] groups = new Class<?>[] { PreDelete.class };
EntityPersister prstr = session.getEntityPersister(null, entity);
Validator validator = getValidator(prstr, session);
validate(validator, entity, session, groups);
}
}
@Override
public void onPreRecreateCollection(PreCollectionRecreateEvent event) {
// System.out
// .println("Recreate. Role: " + event.getCollection().getRole());
}
private void validate(AbstractPreDatabaseOperationEvent event,
Class<?>[] groups) {
EntityPersister persister = event.getPersister();
EventSource session = event.getSession();
Validator validator = getValidator(persister, session);
validate(validator, event.getEntity(), session, groups);
}
private Validator getValidator(EntityPersister persister,
EventSource session) {
SessionFactoryImplementor sessionFactory = session.getFactory();
TraversableResolver tr = new HibernateTraversableResolver(
persister, associationsPerEntityPersister, sessionFactory);
ConstraintValidatorFactory validatorFactory =
new EventSourceAwareConstraintValidatorFactory(session);
Validator validator = factory.usingContext()
.traversableResolver(tr)
.constraintValidatorFactory(validatorFactory)
.messageInterpolator(messageInterpolator)
.getValidator();
return validator;
}
private <T> void validate(Validator validator, T object,
EventSource session, Class<?>[] groups) {
EntityMode mode = session.getEntityMode();
if (object == null || mode != EntityMode.POJO) return;
if (groups.length == 0) return;
FlushMode oldMode = session.getFlushMode();
try {
// If the session is used to query data, then another flush could be
// triggered which will prompt validation (again) and throw us into
// an infinite loop. Avoid this.
session.setFlushMode(FlushMode.MANUAL);
final Set<ConstraintViolation<T>> constraintViolations =
validator.validate(object, groups);
handleViolations(constraintViolations, groups);
} finally {
session.setFlushMode(oldMode);
}
}
private <T> void handleViolations(
Set<ConstraintViolation<T>> constraintViolations, Class<?>[] groups) {
if (constraintViolations.isEmpty()) return;
// TODO: include the bean being validated and the type of
// action that was trying to be performed?
// TODO: put some of this code in another separate class
// that can be called directly (not through a listener)
// through a specific action.
// TODO: add a tag for validations to be performed on the
// server (only) and not locally...
Set<ConstraintViolation<?>> localizedViolations =
new HashSet<ConstraintViolation<?>>(
constraintViolations.size());
StringBuilder builder = new StringBuilder();
builder.append("validation failed for groups: ");
builder.append(Arrays.toString(groups));
for (ConstraintViolation<T> violation : constraintViolations) {
// if (log.isTraceEnabled()) {
// log.trace(violation.toString());
// }
ConstraintViolation<T> localizedViolation =
new LocalizedConstraintViolation<T>(violation);
localizedViolations.add(localizedViolation);
builder.append("\r\n");
builder.append(violation.getLeafBean().getClass().getName());
builder.append(":");
builder.append(violation.getMessage());
}
throw new ConstraintViolationException(builder.toString(),
localizedViolations);
}
}