package org.molgenis.data.validation;
import org.molgenis.data.*;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.support.QueryImpl;
import org.molgenis.util.HugeMap;
import org.molgenis.util.HugeSet;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static java.lang.String.format;
import static java.util.Collections.*;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.data.RepositoryCapability.*;
import static org.molgenis.data.support.EntityTypeUtils.*;
public class RepositoryValidationDecorator extends AbstractRepositoryDecorator<Entity>
{
private enum ValidationMode
{
ADD, UPDATE
}
private final DataService dataService;
private final Repository<Entity> decoratedRepository;
private final EntityAttributesValidator entityAttributesValidator;
private final ExpressionValidator expressionValidator;
public RepositoryValidationDecorator(DataService dataService, Repository<Entity> repository,
EntityAttributesValidator entityAttributesValidator, ExpressionValidator expressionValidator)
{
this.dataService = requireNonNull(dataService);
this.decoratedRepository = requireNonNull(repository);
this.entityAttributesValidator = requireNonNull(entityAttributesValidator);
this.expressionValidator = requireNonNull(expressionValidator);
}
@Override
protected Repository<Entity> delegate()
{
return decoratedRepository;
}
@Override
public void update(Entity entity)
{
try (ValidationResource validationResource = new ValidationResource())
{
entity = validate(Stream.of(entity), validationResource, ValidationMode.UPDATE).findFirst().get();
}
decoratedRepository.update(entity);
}
@Override
public void update(Stream<Entity> entities)
{
try (ValidationResource validationResource = new ValidationResource())
{
entities = validate(entities, validationResource, ValidationMode.UPDATE);
decoratedRepository.update(entities);
}
}
@Override
public void add(Entity entity)
{
try (ValidationResource validationResource = new ValidationResource())
{
entity = validate(Stream.of(entity), validationResource, ValidationMode.ADD).findFirst().get();
}
decoratedRepository.add(entity);
}
@Override
public Integer add(Stream<Entity> entities)
{
try (ValidationResource validationResource = new ValidationResource())
{
entities = validate(entities, validationResource, ValidationMode.ADD);
return decoratedRepository.add(entities);
}
}
private Stream<Entity> validate(Stream<Entity> entities, ValidationResource validationResource,
ValidationMode validationMode)
{
// prepare validation
initValidation(validationResource, validationMode);
boolean validateRequired = !getCapabilities().contains(VALIDATE_NOTNULL_CONSTRAINT);
boolean validateUniqueness = !getCapabilities().contains(VALIDATE_UNIQUE_CONSTRAINT);
boolean validateReadonly = !getCapabilities().contains(VALIDATE_READONLY_CONSTRAINT);
// add validation operation to stream
return entities.filter(entity ->
{
validationResource.incrementRow();
validateEntityValueTypes(entity, validationResource);
// other validation steps might not be able to handle invalid data types, stop here
if (validationResource.hasViolations())
{
throw new MolgenisValidationException(validationResource.getViolations());
}
if (validateRequired)
{
validateEntityValueRequired(entity, validationResource);
}
if (validateUniqueness)
{
validateEntityValueUniqueness(entity, validationResource, validationMode);
}
validateEntityValueReferences(entity, validationResource);
if (validateReadonly && validationMode == ValidationMode.UPDATE)
{
validateEntityValueReadOnly(entity, validationResource);
}
if (validationResource.hasViolations())
{
throw new MolgenisValidationException(validationResource.getViolations());
}
return true;
});
}
private void initValidation(ValidationResource validationResource, ValidationMode validationMode)
{
initRequiredValueValidation(validationResource);
initReferenceValidation(validationResource);
initUniqueValidation(validationResource);
if (validationMode == ValidationMode.UPDATE)
{
initReadonlyValidation(validationResource);
}
}
private void initRequiredValueValidation(ValidationResource validationResource)
{
if (!getCapabilities().contains(VALIDATE_NOTNULL_CONSTRAINT))
{
List<Attribute> requiredValueAttrs = stream(getEntityType().getAtomicAttributes().spliterator(), false)
.filter(attr -> !attr.isNillable() && attr.getExpression() == null).collect(toList());
validationResource.setRequiredValueAttrs(requiredValueAttrs);
}
}
private void initReferenceValidation(ValidationResource validationResource)
{
// get reference attrs
List<Attribute> refAttrs;
if (!getCapabilities().contains(VALIDATE_REFERENCE_CONSTRAINT))
{
// get reference attrs
refAttrs = stream(getEntityType().getAtomicAttributes().spliterator(), false)
.filter(attr -> isReferenceType(attr) && attr.getExpression() == null).collect(toList());
}
else
{
// validate cross-repository collection reference constraints. the decorated repository takes care of
// validating other reference constraints
String backend = dataService.getMeta().getBackend(getEntityType()).getName();
refAttrs = stream(getEntityType().getAtomicAttributes().spliterator(), false)
.filter(attr -> isReferenceType(attr) && attr.getExpression() == null && isDifferentBackend(backend,
attr)).collect(toList());
}
// get referenced entity ids
if (!refAttrs.isEmpty())
{
Map<String, HugeSet<Object>> refEntitiesIds = new HashMap<>();
refAttrs.forEach(refAttr ->
{
EntityType refEntityType = refAttr.getRefEntity();
String refEntityName = refEntityType.getName();
HugeSet<Object> refEntityIds = refEntitiesIds.get(refEntityName);
if (refEntityIds == null)
{
refEntityIds = new HugeSet<>();
refEntitiesIds.put(refEntityName, refEntityIds);
Query<Entity> q = new QueryImpl<>()
.fetch(new Fetch().field(refEntityType.getIdAttribute().getName()));
for (Iterator<Entity> it = dataService.findAll(refEntityName, q).iterator(); it.hasNext(); )
{
refEntityIds.add(it.next().getIdValue());
}
}
});
validationResource.setRefEntitiesIds(refEntitiesIds);
}
validationResource.setSelfReferencing(refAttrs.stream()
.anyMatch(refAttr -> refAttr.getRefEntity().getName().equals(getEntityType().getName())));
validationResource.setRefAttrs(refAttrs);
}
private boolean isDifferentBackend(String backend, Attribute attr)
{
EntityType refEntity = attr.getRefEntity();
String refEntityBackend = dataService.getMeta().getBackend(refEntity).getName();
return !backend.equals(refEntityBackend);
}
private void initUniqueValidation(ValidationResource validationResource)
{
if (!getCapabilities().contains(VALIDATE_UNIQUE_CONSTRAINT))
{
// get unique attributes
List<Attribute> uniqueAttrs = stream(getEntityType().getAtomicAttributes().spliterator(), false)
.filter(attr -> attr.isUnique() && attr.getExpression() == null).collect(toList());
// get existing values for each attributes
if (!uniqueAttrs.isEmpty())
{
Map<String, HugeMap<Object, Object>> uniqueAttrsValues = new HashMap<>();
Fetch fetch = new Fetch();
uniqueAttrs.forEach(uniqueAttr ->
{
uniqueAttrsValues.put(uniqueAttr.getName(), new HugeMap<>());
fetch.field(uniqueAttr.getName());
});
Query<Entity> q = new QueryImpl<>().fetch(fetch);
decoratedRepository.findAll(q).forEach(entity ->
{
uniqueAttrs.forEach(uniqueAttr ->
{
HugeMap<Object, Object> uniqueAttrValues = uniqueAttrsValues.get(uniqueAttr.getName());
Object attrValue = entity.get(uniqueAttr.getName());
if (attrValue != null)
{
if (isSingleReferenceType(uniqueAttr))
{
attrValue = ((Entity) attrValue).getIdValue();
}
uniqueAttrValues.put(attrValue, entity.getIdValue());
}
});
});
validationResource.setUniqueAttrsValues(uniqueAttrsValues);
}
validationResource.setUniqueAttrs(uniqueAttrs);
}
}
private void initReadonlyValidation(ValidationResource validationResource)
{
if (!getCapabilities().contains(VALIDATE_READONLY_CONSTRAINT))
{
String idAttrName = getEntityType().getIdAttribute().getName();
List<Attribute> readonlyAttrs = stream(getEntityType().getAtomicAttributes().spliterator(), false)
.filter(attr -> attr.isReadOnly() && attr.getExpression() == null && !attr.isMappedBy() && !attr
.getName().equals(idAttrName)).collect(toList());
validationResource.setReadonlyAttrs(readonlyAttrs);
}
}
private void validateEntityValueRequired(Entity entity, ValidationResource validationResource)
{
validationResource.getRequiredValueAttrs().forEach(nonNillableAttr ->
{
Object value = entity.get(nonNillableAttr.getName());
if (value == null || (isMultipleReferenceType(nonNillableAttr) && !entity
.getEntities(nonNillableAttr.getName()).iterator().hasNext()))
{
ConstraintViolation constraintViolation = new ConstraintViolation(
format("The attribute '%s' of entity '%s' can not be null.", nonNillableAttr.getName(),
getName()), nonNillableAttr, Integer.valueOf(validationResource.getRow()).longValue());
validationResource.addViolation(constraintViolation);
}
});
}
private void validateEntityValueTypes(Entity entity, ValidationResource validationResource)
{
// entity attributes validation
Set<ConstraintViolation> attrViolations = entityAttributesValidator.validate(entity, getEntityType());
if (attrViolations != null && !attrViolations.isEmpty())
{
attrViolations.forEach(attrViolation ->
{
validationResource.addViolation(attrViolation);
});
}
}
private void validateEntityValueUniqueness(Entity entity, ValidationResource validationResource,
ValidationMode validationMode)
{
validationResource.getUniqueAttrs().forEach(uniqueAttr ->
{
Object attrValue = entity.get(uniqueAttr.getName());
if (attrValue != null)
{
if (isSingleReferenceType(uniqueAttr))
{
attrValue = ((Entity) attrValue).getIdValue();
}
HugeMap<Object, Object> uniqueAttrValues = validationResource.getUniqueAttrsValues()
.get(uniqueAttr.getName());
Object existingEntityId = uniqueAttrValues.get(attrValue);
if ((validationMode == ValidationMode.ADD && existingEntityId != null) || (
validationMode == ValidationMode.UPDATE && existingEntityId != null && !existingEntityId
.equals(entity.getIdValue())))
{
ConstraintViolation constraintViolation = new ConstraintViolation(
format("Duplicate value '%s' for unique attribute '%s' from entity '%s'", attrValue,
uniqueAttr.getName(), getName()), uniqueAttr,
Long.valueOf(validationResource.getRow()));
validationResource.addViolation(constraintViolation);
}
else
{
uniqueAttrValues.put(attrValue, entity.getIdValue());
}
}
});
}
private void validateEntityValueReferences(Entity entity, ValidationResource validationResource)
{
validationResource.getRefAttrs().forEach(refAttr ->
{
HugeSet<Object> refEntityIds = validationResource.getRefEntitiesIds().get(refAttr.getRefEntity().getName());
Iterable<Entity> refEntities;
if (isSingleReferenceType(refAttr))
{
Entity refEntity = entity.getEntity(refAttr.getName());
if (refEntity != null)
{
refEntities = singleton(refEntity);
}
else
{
refEntities = emptyList();
}
}
else
{
refEntities = entity.getEntities(refAttr.getName());
}
for (Entity refEntity : refEntities)
{
if (!refEntityIds.contains(refEntity.getIdValue()))
{
boolean selfReference = entity.getEntityType().getName().equals(refAttr.getRefEntity().getName());
if (!(selfReference && entity.getIdValue().equals(refEntity.getIdValue())))
{
String message = String.format("Unknown xref value '%s' for attribute '%s' of entity '%s'.",
DataConverter.toString(refEntity.getIdValue()), refAttr.getName(), getName());
ConstraintViolation constraintViolation = new ConstraintViolation(message, refAttr,
Long.valueOf(validationResource.getRow()));
validationResource.addViolation(constraintViolation);
}
}
}
// only do if self reference
if (validationResource.isSelfReferencing())
{
validationResource.addRefEntityId(getName(), entity.getIdValue());
}
});
}
@SuppressWarnings("unchecked")
private void validateEntityValueReadOnly(Entity entity, ValidationResource validationResource)
{
if (validationResource.getReadonlyAttrs().isEmpty())
{
return;
}
Entity entityToUpdate = findOneById(entity.getIdValue());
validationResource.getReadonlyAttrs().forEach(readonlyAttr ->
{
Object value = entity.get(readonlyAttr.getName());
Object existingValue = entityToUpdate.get(readonlyAttr.getName());
if (isSingleReferenceType(readonlyAttr))
{
if (value != null)
{
value = ((Entity) value).getIdValue();
}
if (existingValue != null)
{
existingValue = ((Entity) existingValue).getIdValue();
}
}
else if (isMultipleReferenceType(readonlyAttr))
{
value = stream(entity.getEntities(readonlyAttr.getName()).spliterator(), false).map(Entity::getIdValue)
.collect(toList());
existingValue = stream(entityToUpdate.getEntities(readonlyAttr.getName()).spliterator(), false)
.map(Entity::getIdValue).collect(toList());
}
if (value != null && existingValue != null && !value.equals(existingValue))
{
validationResource.addViolation(new ConstraintViolation(
format("The attribute '%s' of entity '%s' can not be changed it is readonly.",
readonlyAttr.getName(), getName()), Long.valueOf(validationResource.getRow())));
}
});
}
/**
* Container with validation data used during stream validation
*/
private static class ValidationResource implements AutoCloseable
{
private AtomicInteger rowNr = new AtomicInteger();
private List<Attribute> requiredValueAttrs;
private List<Attribute> refAttrs;
private Map<String, HugeSet<Object>> refEntitiesIds;
private List<Attribute> uniqueAttrs;
private Map<String, HugeMap<Object, Object>> uniqueAttrsValues;
private List<Attribute> readonlyAttrs;
private boolean selfReferencing;
private Set<ConstraintViolation> violations;
public ValidationResource()
{
rowNr = new AtomicInteger();
}
public int getRow()
{
return rowNr.get();
}
public void incrementRow()
{
rowNr.incrementAndGet();
}
public List<Attribute> getRequiredValueAttrs()
{
return requiredValueAttrs != null ? unmodifiableList(requiredValueAttrs) : emptyList();
}
public void setRequiredValueAttrs(List<Attribute> requiredValueAttrs)
{
this.requiredValueAttrs = requiredValueAttrs;
}
public List<Attribute> getRefAttrs()
{
return unmodifiableList(refAttrs);
}
public void setRefAttrs(List<Attribute> refAttrs)
{
this.refAttrs = refAttrs;
}
public Map<String, HugeSet<Object>> getRefEntitiesIds()
{
return refEntitiesIds != null ? unmodifiableMap(refEntitiesIds) : emptyMap();
}
public void setRefEntitiesIds(Map<String, HugeSet<Object>> refEntitiesIds)
{
this.refEntitiesIds = refEntitiesIds;
}
public void addRefEntityId(String name, Object idValue)
{
HugeSet<Object> refEntityIds = refEntitiesIds.get(name);
// only add entity id if this validation run requires entity
if (refEntityIds != null)
{
refEntityIds.add(idValue);
}
}
public List<Attribute> getUniqueAttrs()
{
return uniqueAttrs != null ? unmodifiableList(uniqueAttrs) : emptyList();
}
public void setUniqueAttrs(List<Attribute> uniqueAttrs)
{
this.uniqueAttrs = uniqueAttrs;
}
public Map<String, HugeMap<Object, Object>> getUniqueAttrsValues()
{
return uniqueAttrsValues != null ? unmodifiableMap(uniqueAttrsValues) : emptyMap();
}
public void setUniqueAttrsValues(Map<String, HugeMap<Object, Object>> uniqueAttrsValues)
{
this.uniqueAttrsValues = uniqueAttrsValues;
}
public List<Attribute> getReadonlyAttrs()
{
return readonlyAttrs != null ? unmodifiableList(readonlyAttrs) : emptyList();
}
public void setReadonlyAttrs(List<Attribute> readonlyAttrs)
{
this.readonlyAttrs = readonlyAttrs;
}
public void setSelfReferencing(boolean selfReferencing)
{
this.selfReferencing = selfReferencing;
}
public boolean isSelfReferencing()
{
return selfReferencing;
}
public boolean hasViolations()
{
return violations != null && !violations.isEmpty();
}
public void addViolation(ConstraintViolation constraintViolation)
{
if (violations == null)
{
violations = new LinkedHashSet<>();
}
violations.add(constraintViolation);
}
public Set<ConstraintViolation> getViolations()
{
return violations != null ? unmodifiableSet(violations) : emptySet();
}
@Override
public void close()
{
if (refEntitiesIds != null)
{
for (HugeSet<Object> refEntityIds : refEntitiesIds.values())
{
try
{
refEntityIds.close();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
if (uniqueAttrsValues != null)
{
for (HugeMap<Object, Object> uniqueAttrValues : uniqueAttrsValues.values())
{
try
{
uniqueAttrValues.close();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
}
}
}