package edu.ualberta.med.biobank.validator.constraint.model.impl;
import java.sql.Connection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import edu.ualberta.med.biobank.model.ContainerLabelingScheme;
import edu.ualberta.med.biobank.model.ContainerPosition;
import edu.ualberta.med.biobank.model.ContainerType;
import edu.ualberta.med.biobank.model.SpecimenPosition;
import edu.ualberta.med.biobank.model.SpecimenType;
import edu.ualberta.med.biobank.util.NullHelper;
import edu.ualberta.med.biobank.validator.EventSourceAwareConstraintValidator;
import edu.ualberta.med.biobank.validator.constraint.model.ValidContainerType;
public class ValidContainerTypeValidator
extends EventSourceAwareConstraintValidator<Object>
implements ConstraintValidator<ValidContainerType, Object> {
public static final String MULTIPLE_CHILD_TYPES =
"{edu.ualberta.med.biobank.model.ContainerType.ValidContainerType.multipleChildTypes}";
public static final String OVER_CAPACITY =
"{edu.ualberta.med.biobank.model.ContainerType.ValidContainerType.overCapacity}";
public static final String ILLEGAL_CHANGE =
"{edu.ualberta.med.biobank.model.ContainerType.ValidContainerType.illegalChange}";
public static final String ILLEGAL_CHILD_CT_REMOVE =
"{edu.ualberta.med.biobank.model.ContainerType.ValidContainerType.illegalChildContainerTypeRemove}";
public static final String ILLEGAL_ST_REMOVE =
"{edu.ualberta.med.biobank.model.ContainerType.ValidContainerType.illegalSpecimenTypeRemove}";
@Override
public void initialize(ValidContainerType annotation) {
}
@Override
public boolean isValidInEventSource(Object value,
ConstraintValidatorContext context) {
if (value == null) return true;
if (!(value instanceof ContainerType)) return false;
context.disableDefaultConstraintViolation();
ContainerType ct = (ContainerType) value;
ContainerType oldCt = getOldContainerTypeOrNull(ct);
boolean isValid = true;
isValid &= checkCapacity(ct, context);
isValid &= checkChildrenTypes(ct, context);
if (oldCt != null) {
isValid &= checkChanges(ct, oldCt, context);
isValid &= checkRemovedChildContainerTypes(ct, oldCt, context);
isValid &= checkRemovedSpecimenTypes(ct, oldCt, context);
}
return isValid;
}
private boolean checkChildrenTypes(ContainerType ct,
ConstraintValidatorContext context) {
// if either set is initialised we must load the other one to be sure,
// otherwise assume this check passed before and still does
if (Hibernate.isInitialized(ct.getChildContainerTypes()) ||
Hibernate.isInitialized(ct.getSpecimenTypes())) {
if (!ct.getChildContainerTypes().isEmpty()
&& !ct.getSpecimenTypes().isEmpty()) {
context
.buildConstraintViolationWithTemplate(MULTIPLE_CHILD_TYPES)
.addNode("childContainerTypes")
.addNode("specimenTypes")
.addConstraintViolation();
return false;
}
}
return true;
}
private boolean checkCapacity(ContainerType ct,
ConstraintValidatorContext context) {
ContainerLabelingScheme scheme = ct.getChildLabelingScheme();
if (scheme != null && !scheme.canLabel(ct.getCapacity())) {
context.buildConstraintViolationWithTemplate(OVER_CAPACITY)
.addNode("childLabelingScheme")
// TODO: any way to mark rowCapacity and colCapacity?
.addNode("capacity")
.addConstraintViolation();
return false;
}
return true;
}
private ContainerType getOldContainerTypeOrNull(ContainerType ct) {
if (ct.isNew()) return null;
ContainerType oldCt = null;
// Get the old value in the same transaction in case that transaction
// has not been committed yet
Connection conn = getEventSource().connection();
Session newSession = getEventSource().getSessionFactory()
.openSession(conn);
try {
oldCt = (ContainerType) newSession
.createCriteria(ContainerType.class)
.add(Restrictions.idEq(ct.getId()))
.uniqueResult();
} catch (HibernateException e) {
}
return oldCt;
}
private boolean checkChanges(ContainerType ct, ContainerType oldCt,
ConstraintValidatorContext context) {
if (!isUsed(ct)) return true;
boolean isValid = true;
// TODO: should be able to change capacity and labeling scheme as long
// as it does not cause any existing containers or specimens to have a
// label change. For example, it is probably okay to add and remove
// rows, but not columns if more than one row is filled (assuming
// labeling is done row by row)
isValid &= NullHelper.safeEquals(ct.getCapacity(), oldCt.getCapacity());
isValid &= NullHelper.safeEquals(ct.getTopLevel(), oldCt.getTopLevel());
isValid &= NullHelper.safeEquals(ct.getChildLabelingScheme(),
oldCt.getChildLabelingScheme());
if (!isValid) {
context.buildConstraintViolationWithTemplate(ILLEGAL_CHANGE)
.addNode("capacity")
.addNode("topLevel")
.addNode("childLabelingScheme")
.addConstraintViolation();
}
return isValid;
}
private boolean isUsed(ContainerType ct) {
return isUsed(ct, SpecimenPosition.class, "containerType")
|| isUsed(ct, ContainerPosition.class, "parentContainerType");
}
private boolean isUsed(ContainerType ct, Class<?> by, String property) {
List<?> results = getEventSource()
.createCriteria(by)
.add(Restrictions.eq(property, ct))
.setProjection(Projections.rowCount())
.list();
Number count = (Number) results.iterator().next();
boolean isUsed = count.intValue() != 0;
return isUsed;
}
private boolean checkRemovedChildContainerTypes(ContainerType ct,
ContainerType oldCt, ConstraintValidatorContext context) {
Set<ContainerType> removed = new HashSet<ContainerType>();
removed.addAll(oldCt.getChildContainerTypes());
removed.removeAll(ct.getChildContainerTypes());
if (removed.isEmpty()) return true;
List<?> results = getEventSource()
.createCriteria(ContainerPosition.class)
.add(Restrictions.in("containerType", removed))
.add(Restrictions.eq("parentContainerType", ct))
.setProjection(Projections.rowCount())
.list();
Number count = (Number) results.iterator().next();
boolean isValid = count.intValue() == 0;
if (!isValid) {
context.buildConstraintViolationWithTemplate(
ILLEGAL_CHILD_CT_REMOVE)
.addNode("childContainerTypes")
.addConstraintViolation();
}
return isValid;
}
private boolean checkRemovedSpecimenTypes(ContainerType ct,
ContainerType oldCt, ConstraintValidatorContext context) {
Set<SpecimenType> removed = new HashSet<SpecimenType>();
removed.addAll(oldCt.getSpecimenTypes());
removed.removeAll(ct.getSpecimenTypes());
if (removed.isEmpty()) return true;
List<?> results = getEventSource()
.createCriteria(SpecimenPosition.class)
.add(Restrictions.in("specimenType", removed))
.add(Restrictions.eq("containerType", ct))
.setProjection(Projections.rowCount())
.list();
Number count = (Number) results.iterator().next();
boolean isValid = count.intValue() == 0;
if (!isValid) {
context.buildConstraintViolationWithTemplate(
ILLEGAL_ST_REMOVE)
.addNode("specimenTypes")
.addConstraintViolation();
}
return isValid;
}
}