package org.molgenis.data.validation;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.impl.EmailValidator;
import org.molgenis.data.Entity;
import org.molgenis.data.Range;
import org.molgenis.data.meta.AttributeType;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static com.google.api.client.util.Lists.newArrayList;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.molgenis.data.meta.AttributeType.*;
/**
* Attribute data type validator.
* <p>
* Does not check if xref,mref, categorical values are present. That happens in the EntityValidator.
*/
@Component
public class EntityAttributesValidator
{
private final ExpressionValidator expressionValidator;
private EmailValidator emailValidator;
@Autowired
public EntityAttributesValidator(ExpressionValidator expressionValidator)
{
this.expressionValidator = requireNonNull(expressionValidator);
}
public Set<ConstraintViolation> validate(Entity entity, EntityType meta)
{
Set<ConstraintViolation> violations = checkValidationExpressions(entity, meta);
for (Attribute attr : meta.getAtomicAttributes())
{
ConstraintViolation violation = null;
AttributeType attrType = attr.getDataType();
switch (attrType)
{
case EMAIL:
violation = checkEmail(entity, attr, meta);
break;
case BOOL:
violation = checkBoolean(entity, attr, meta);
break;
case DATE:
violation = checkDate(entity, attr, meta);
break;
case DATE_TIME:
violation = checkDateTime(entity, attr, meta);
break;
case DECIMAL:
violation = checkDecimal(entity, attr, meta);
break;
case HYPERLINK:
violation = checkHyperlink(entity, attr, meta);
break;
case INT:
violation = checkInt(entity, attr, meta);
if ((violation == null) && (attr.getRange() != null))
{
violation = checkRange(entity, attr, meta);
}
break;
case LONG:
violation = checkLong(entity, attr, meta);
if ((violation == null) && (attr.getRange() != null))
{
violation = checkRange(entity, attr, meta);
}
break;
case ENUM:
violation = checkEnum(entity, attr, meta);
break;
case HTML:
violation = checkText(entity, attr, meta, HTML);
break;
case SCRIPT:
violation = checkText(entity, attr, meta, SCRIPT);
break;
case TEXT:
violation = checkText(entity, attr, meta, TEXT);
break;
case STRING:
violation = checkText(entity, attr, meta, STRING);
break;
case CATEGORICAL:
case FILE:
case XREF:
violation = checkXref(entity, attr, meta);
break;
case CATEGORICAL_MREF:
case MREF:
case ONE_TO_MANY:
violation = checkMref(entity, attr, meta);
break;
case COMPOUND:
// no op
break;
default:
throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString()));
}
if (violation != null)
{
violations.add(violation);
}
}
return violations;
}
private ConstraintViolation checkMref(Entity entity, Attribute attr, EntityType entityType)
{
Iterable<Entity> refEntities;
try
{
refEntities = entity.getEntities(attr.getName());
}
catch (Exception e)
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity, expected an entity list.");
}
if (refEntities == null)
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity, expected an entity list.");
}
for (Entity refEntity : refEntities)
{
if (refEntity == null)
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity, null is not allowed");
}
if (!refEntity.getEntityType().getName().equals(attr.getRefEntity().getName()))
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity type.");
}
}
return null;
}
private ConstraintViolation checkXref(Entity entity, Attribute attr, EntityType entityType)
{
Entity refEntity;
try
{
refEntity = entity.getEntity(attr.getName());
}
catch (Exception e)
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity.");
}
if (refEntity == null)
{
return null;
}
if (!refEntity.getEntityType().getName().equals(attr.getRefEntity().getName()))
{
return createConstraintViolation(entity, attr, entityType, "Not a valid entity type.");
}
return null;
}
private Set<ConstraintViolation> checkValidationExpressions(Entity entity, EntityType meta)
{
List<String> validationExpressions = new ArrayList<>();
List<Attribute> expressionAttributes = new ArrayList<>();
for (Attribute attribute : meta.getAtomicAttributes())
{
if (StringUtils.isNotBlank(attribute.getValidationExpression()))
{
expressionAttributes.add(attribute);
validationExpressions.add(attribute.getValidationExpression());
}
}
Set<ConstraintViolation> violations = new LinkedHashSet<>();
if (!validationExpressions.isEmpty())
{
List<Boolean> results = expressionValidator.resolveBooleanExpressions(validationExpressions, entity);
for (int i = 0; i < results.size(); i++)
{
if (!results.get(i))
{
violations.add(createConstraintViolation(entity, expressionAttributes.get(i), meta,
format("Offended expression: %s", validationExpressions.get(i))));
}
}
}
return violations;
}
private ConstraintViolation checkEmail(Entity entity, Attribute attribute, EntityType entityType)
{
String email = entity.getString(attribute.getName());
if (email == null)
{
return null;
}
if (emailValidator == null)
{
emailValidator = new EmailValidator();
}
if (!emailValidator.isValid(email, null))
{
return createConstraintViolation(entity, attribute, entityType, "Not a valid e-mail address.");
}
if (email.length() > EMAIL.getMaxLength())
{
return createConstraintViolation(entity, attribute, entityType);
}
return null;
}
private static ConstraintViolation checkBoolean(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getBoolean(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private static ConstraintViolation checkDateTime(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getUtilDate(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private static ConstraintViolation checkDate(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getDate(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private static ConstraintViolation checkDecimal(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getDouble(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private ConstraintViolation checkHyperlink(Entity entity, Attribute attribute, EntityType entityType)
{
String link = entity.getString(attribute.getName());
if (link == null)
{
return null;
}
try
{
new URI(link);
}
catch (URISyntaxException e)
{
return createConstraintViolation(entity, attribute, entityType, "Not a valid hyperlink.");
}
if (link.length() > HYPERLINK.getMaxLength())
{
return createConstraintViolation(entity, attribute, entityType);
}
return null;
}
private static ConstraintViolation checkInt(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getInt(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private static ConstraintViolation checkLong(Entity entity, Attribute attribute, EntityType entityType)
{
try
{
entity.getLong(attribute.getName());
return null;
}
catch (Exception e)
{
return createConstraintViolation(entity, attribute, entityType);
}
}
private static ConstraintViolation checkRange(Entity entity, Attribute attr, EntityType entityType)
{
Range range = attr.getRange();
Long value;
switch (attr.getDataType())
{
case INT:
Integer intValue = entity.getInt(attr.getName());
value = intValue != null ? intValue.longValue() : null;
break;
case LONG:
value = entity.getLong(attr.getName());
break;
default:
throw new RuntimeException(
format("Range not allowed for data type [%s]", attr.getDataType().toString()));
}
if ((value != null) && ((range.getMin() != null && value < range.getMin()) || (range.getMax() != null
&& value > range.getMax())))
{
return createConstraintViolation(entity, attr, entityType);
}
return null;
}
private static ConstraintViolation checkText(Entity entity, Attribute attribute, EntityType meta,
AttributeType fieldType)
{
String text = entity.getString(attribute.getName());
if (text == null)
{
return null;
}
if (text.length() > fieldType.getMaxLength())
{
return createConstraintViolation(entity, attribute, meta);
}
return null;
}
private ConstraintViolation checkEnum(Entity entity, Attribute attribute, EntityType entityType)
{
String value = entity.getString(attribute.getName());
if (value != null)
{
List<String> enumOptions = attribute.getEnumOptions();
if (!enumOptions.contains(value))
{
return createConstraintViolation(entity, attribute, entityType,
"Value must be one of " + enumOptions.toString());
}
}
return null;
}
private static ConstraintViolation createConstraintViolation(Entity entity, Attribute attribute,
EntityType entityType)
{
String message = format("Invalid %s value '%s' for attribute '%s' of entity '%s'.",
attribute.getDataType().toString().toLowerCase(), entity.get(attribute.getName()), attribute.getLabel(),
entityType.getName());
Range range = attribute.getRange();
if (range != null)
{
message += format("Value must be between %d and %d", range.getMin(), range.getMax());
}
Long maxLength = attribute.getDataType().getMaxLength();
if (maxLength != null)
{
message += format("Value must be less than or equal to %d characters", maxLength);
}
return new ConstraintViolation(message, entity.get(attribute.getName()), entity, attribute, entityType, null);
}
private ConstraintViolation createConstraintViolation(Entity entity, Attribute attribute, EntityType entityType,
String message)
{
String dataValue = getDataValuesForType(entity, attribute).toString();
String fullMessage = format("Invalid [%s] value [%s] for attribute [%s] of entity [%s] with type [%s].",
attribute.getDataType().toString().toLowerCase(), dataValue, attribute.getLabel(),
entity.getLabelValue(), entityType.getName());
fullMessage += " " + message;
return new ConstraintViolation(fullMessage, dataValue, entity, attribute, entityType, null);
}
private Object getDataValuesForType(Entity entity, Attribute attribute)
{
String attributeName = attribute.getName();
switch (attribute.getDataType())
{
case DATE:
case DATE_TIME:
return entity.getUtilDate(attributeName);
case BOOL:
return entity.getBoolean(attributeName);
case DECIMAL:
case LONG:
case INT:
return entity.getInt(attributeName);
case HYPERLINK:
case ENUM:
case HTML:
case TEXT:
case SCRIPT:
case EMAIL:
case STRING:
return entity.getString(attributeName);
case CATEGORICAL:
case XREF:
case FILE:
Entity refEntity = entity.getEntity(attributeName);
if (refEntity != null) return refEntity.getIdValue();
else return "";
case CATEGORICAL_MREF:
case MREF:
List<String> mrefValues = newArrayList();
for (Entity mrefEntity : entity.getEntities(attributeName))
{
if (mrefEntity != null)
{
mrefValues.add(mrefEntity.getIdValue().toString());
}
}
return mrefValues;
case COMPOUND:
return "";
default:
return "";
}
}
}