package org.molgenis.data.validation; import org.molgenis.data.Entity; import org.molgenis.data.EntityManager; import org.molgenis.data.Query; import org.molgenis.data.QueryRule; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.EntityType; import org.molgenis.file.model.FileMeta; import org.molgenis.util.MolgenisDateFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.text.ParseException; import java.util.Date; import java.util.List; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; import static org.molgenis.util.MolgenisDateFormat.getDateFormat; import static org.molgenis.util.MolgenisDateFormat.getDateTimeFormat; /** * Validates {@link Query queries} based on the {@link EntityType entity type} that will be queried. Converts query * values to the correct class type if possible. * * @see <a href="https://github.com/molgenis/molgenis/issues/5248">https://github.com/molgenis/molgenis/issues/5248</a> */ @Component public class QueryValidator { private final EntityManager entityManager; @Autowired public QueryValidator(EntityManager entityManager) { this.entityManager = requireNonNull(entityManager); } /** * Validates query based on the given entity type, converts query values to the expected type if necessary. * * @param query query * @param entityType entity type * @throws MolgenisValidationException if query is invalid */ public void validate(Query<? extends Entity> query, EntityType entityType) { query.getRules().forEach(queryRule -> validateQueryRule(queryRule, entityType)); } private void validateQueryRule(QueryRule queryRule, EntityType entityType) { QueryRule.Operator operator = queryRule.getOperator(); switch (operator) { case AND: case NOT: case OR: break; case EQUALS: case FUZZY_MATCH: case FUZZY_MATCH_NGRAM: case GREATER: case GREATER_EQUAL: case LESS: case LESS_EQUAL: case LIKE: { Attribute attr = getQueryRuleAttribute(queryRule, entityType); Object value = toQueryRuleValue(queryRule.getValue(), attr); queryRule.setValue(value); break; } case SEARCH: { Object queryRuleValue = queryRule.getValue(); if (queryRuleValue != null && !(queryRuleValue instanceof String)) { // fix value type queryRule.setValue(queryRuleValue.toString()); } break; } case IN: case RANGE: { Attribute attr = getQueryRuleAttribute(queryRule, entityType); Object queryRuleValue = queryRule.getValue(); if (queryRuleValue != null) { if (!(queryRuleValue instanceof Iterable<?>)) { throw new MolgenisValidationException(new ConstraintViolation( format("Query rule with operator [%s] value is of type [%s] instead of [Iterable]", operator, queryRuleValue.getClass().getSimpleName()))); } // fix value types Iterable<?> queryRuleValues = (Iterable<?>) queryRuleValue; List<Object> values = stream(queryRuleValues.spliterator(), false) .map(value -> toQueryRuleValue(value, attr)).collect(toList()); queryRule.setValue(values); } break; } case DIS_MAX: case NESTED: case SHOULD: queryRule.getNestedRules().forEach(nestedQueryRule -> validateQueryRule(nestedQueryRule, entityType)); break; default: throw new RuntimeException(format("Unknown query operator [%s]", operator.toString())); } } private static Attribute getQueryRuleAttribute(QueryRule queryRule, EntityType entityType) { String queryRuleField = queryRule.getField(); if (queryRuleField == null) { throw new MolgenisValidationException(new ConstraintViolation( format("Query rule with operator [%s] is missing required field", queryRule.getOperator().toString()))); } Attribute attr = entityType.getAttribute(queryRuleField); if (attr == null) { throw new MolgenisValidationException(new ConstraintViolation( format("Query rule field [%s] refers to unknown attribute in entity type [%s]", queryRuleField, entityType.getName()))); } return attr; } private Object toQueryRuleValue(Object queryRuleValue, Attribute attr) { Object value; AttributeType attrType = attr.getDataType(); switch (attrType) { case BOOL: value = convertBool(attr, queryRuleValue); break; case EMAIL: case HTML: case HYPERLINK: case SCRIPT: case STRING: case TEXT: value = convertString(attr, queryRuleValue); break; case ENUM: value = convertEnum(attr, queryRuleValue); break; case CATEGORICAL: case XREF: case CATEGORICAL_MREF: case MREF: case ONE_TO_MANY: value = convertRef(attr, queryRuleValue); break; case DATE: value = convertDate(attr, queryRuleValue); break; case DATE_TIME: value = convertDateTime(attr, queryRuleValue); break; case DECIMAL: value = convertDecimal(attr, queryRuleValue); break; case FILE: value = convertFile(attr, queryRuleValue); break; case INT: value = convertInt(attr, queryRuleValue); break; case LONG: value = convertLong(attr, queryRuleValue); break; case COMPOUND: throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] type [%s] is not allowed", attr.getName(), attrType.toString()))); default: throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString())); } return value; } private static String convertEnum(Attribute attr, Object value) { if (value == null) { return null; } String stringValue; if (value instanceof String) { stringValue = (String) value; } else if (value instanceof Enum) { stringValue = value.toString(); } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Enum.class.getSimpleName()))); } if (!attr.getEnumOptions().contains(stringValue)) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] is not a valid enum option", attr.getName(), stringValue))); } return stringValue; } private static Long convertLong(Attribute attr, Object value) { if (value instanceof Long) { return (Long) value; } if (value == null) { return null; } // try to convert value Long longValue; if (value instanceof String) { try { longValue = Long.valueOf((String) value); } catch (NumberFormatException e) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] cannot be converter to type [%s]", attr.getName(), value, Long.class.getSimpleName()))); } } else if (value instanceof Number) { longValue = ((Number) value).longValue(); } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName()))); } return longValue; } private static Integer convertInt(Attribute attr, Object value) { if (value instanceof Integer) { return (Integer) value; } if (value == null) { return null; } // try to convert value Integer integerValue; if (value instanceof String) { try { integerValue = Integer.valueOf((String) value); } catch (NumberFormatException e) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] cannot be converter to type [%s]", attr.getName(), value, Integer.class.getSimpleName()))); } } else if (value instanceof Number) { integerValue = ((Number) value).intValue(); } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName()))); } return integerValue; } private FileMeta convertFile(Attribute attr, Object paramValue) { Entity entity = convertRef(attr, paramValue); if (entity == null) { return null; } if (!(entity instanceof FileMeta)) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s]", attr.getName(), entity.getClass().getSimpleName(), FileMeta.class.getSimpleName()))); } return (FileMeta) entity; } private static Double convertDecimal(Attribute attr, Object value) { if (value instanceof Double) { return (Double) value; } if (value == null) { return null; } // try to convert value Double doubleValue; if (value instanceof String) { try { doubleValue = Double.valueOf((String) value); } catch (NumberFormatException e) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] cannot be converter to type [%s]", attr.getName(), value, Double.class.getSimpleName()))); } } else if (value instanceof Number) { doubleValue = ((Number) value).doubleValue(); } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Number.class.getSimpleName()))); } return doubleValue; } private static Date convertDateTime(Attribute attr, Object value) { if (value instanceof Date) { return (Date) value; } if (value == null) { return null; } // try to convert value Date dateValue; if (value instanceof String) { String paramStrValue = (String) value; try { dateValue = getDateTimeFormat().parse(paramStrValue); } catch (ParseException e) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] does not match date format [%s]", attr.getName(), paramStrValue, MolgenisDateFormat.getDateTimeFormat().toPattern()))); } } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Date.class.getSimpleName()))); } return dateValue; } private static Date convertDate(Attribute attr, Object value) { if (value instanceof Date) { return (Date) value; } if (value == null) { return null; } // try to convert value Date dateValue; if (value instanceof String) { String paramStrValue = (String) value; try { dateValue = getDateFormat().parse(paramStrValue); } catch (ParseException e) { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] does not match date format [%s]", attr.getName(), paramStrValue, MolgenisDateFormat.getDateFormat().toPattern()))); } } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName()))); } return dateValue; } private Entity convertRef(Attribute attr, Object value) { if (value instanceof Entity) { return (Entity) value; } if (value == null) { return null; } // try to convert value Object idValue = toQueryRuleValue(value, attr.getRefEntity().getIdAttribute()); return entityManager.getReference(attr.getRefEntity(), idValue); } private static String convertString(@SuppressWarnings("unused") Attribute attr, Object value) { if (value instanceof String) { return (String) value; } if (value == null) { return null; } return value.toString(); } private static Boolean convertBool(Attribute attr, Object value) { if (value instanceof Boolean) { return (Boolean) value; } if (value == null) { return null; } Boolean booleanValue; if (value instanceof String) { String stringValue = (String) value; if (stringValue.equalsIgnoreCase(TRUE.toString())) { booleanValue = true; } else if (stringValue.equalsIgnoreCase(FALSE.toString())) { booleanValue = false; } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value [%s] cannot be converter to type [%s]", attr.getName(), value, Boolean.class.getSimpleName()))); } } else { throw new MolgenisValidationException(new ConstraintViolation( format("Attribute [%s] value is of type [%s] instead of [%s] or [%s]", attr.getName(), value.getClass().getSimpleName(), String.class.getSimpleName(), Boolean.class.getSimpleName()))); } return booleanValue; } }