package org.molgenis.data.meta.model;
import org.molgenis.data.Sort;
import org.molgenis.data.meta.AttributeType;
import org.molgenis.data.meta.SystemEntityType;
import org.molgenis.data.support.EntityTypeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
import static org.molgenis.data.meta.AttributeType.*;
import static org.molgenis.data.meta.model.EntityType.AttributeRole.*;
import static org.molgenis.data.meta.model.MetaPackage.PACKAGE_META;
import static org.molgenis.data.meta.model.Package.PACKAGE_SEPARATOR;
import static org.molgenis.data.support.AttributeUtils.getValidIdAttributeTypes;
@Component
public class AttributeMetadata extends SystemEntityType
{
private static final String SIMPLE_NAME = "Attribute";
public static final String ATTRIBUTE_META_DATA = PACKAGE_META + PACKAGE_SEPARATOR + SIMPLE_NAME;
public static final String ID = "id";
public static final String NAME = "name";
public static final String ENTITY = "entity";
public static final String SEQUENCE_NR = "sequenceNr";
public static final String TYPE = "type";
public static final String IS_ID_ATTRIBUTE = "isIdAttribute";
public static final String IS_LABEL_ATTRIBUTE = "isLabelAttribute";
public static final String LOOKUP_ATTRIBUTE_INDEX = "lookupAttributeIndex";
public static final String REF_ENTITY_TYPE = "refEntityType";
/**
* For attributes with data type ONE_TO_MANY defines the attribute in the referenced entity that owns the relationship.
*/
public static final String MAPPED_BY = "mappedBy";
/**
* For attributes with data type ONE_TO_MANY defines how to sort the entity collection.
* Syntax: attribute_name,[ASC | DESC] [;attribute_name,[ASC | DESC]]*
* - If ASC or DESC is not specified, ASC (ascending order) is assumed.
* - If the ordering element is not specified, ordering by the id attribute of the associated entity is assumed.
*/
public static final String ORDER_BY = "orderBy";
public static final String LABEL = "label";
public static final String DESCRIPTION = "description";
public static final String IS_NULLABLE = "isNullable";
public static final String IS_AUTO = "isAuto";
public static final String IS_VISIBLE = "isVisible";
public static final String IS_UNIQUE = "isUnique";
public static final String IS_READ_ONLY = "isReadOnly";
public static final String IS_AGGREGATABLE = "isAggregatable";
public static final String EXPRESSION = "expression";
public static final String ENUM_OPTIONS = "enumOptions";
public static final String RANGE_MIN = "rangeMin";
public static final String RANGE_MAX = "rangeMax";
public static final String PARENT = "parent";
public static final String CHILDREN = "children";
public static final String TAGS = "tags";
public static final String VISIBLE_EXPRESSION = "visibleExpression";
public static final String VALIDATION_EXPRESSION = "validationExpression";
public static final String DEFAULT_VALUE = "defaultValue";
private TagMetadata tagMetadata;
private EntityTypeMetadata entityTypeMeta;
public AttributeMetadata()
{
super(SIMPLE_NAME, PACKAGE_META);
}
public void init()
{
setLabel("Attribute");
setDescription("Meta data for attributes");
addAttribute(ID, ROLE_ID).setVisible(false).setAuto(true).setLabel("Identifier");
addAttribute(NAME, ROLE_LABEL, ROLE_LOOKUP).setNillable(false).setReadOnly(true).setLabel("Name");
addAttribute(ENTITY).setDataType(XREF).setRefEntity(entityTypeMeta).setLabel("Entity").setNillable(false)
.setReadOnly(true);
addAttribute(SEQUENCE_NR).setDataType(INT).setLabel("Sequence number")
.setDescription("Number that defines order of attributes in a entity").setNillable(false);
addAttribute(TYPE).setDataType(ENUM).setEnumOptions(AttributeType.getOptionsLowercase()).setNillable(false)
.setLabel("Data type");
addAttribute(IS_ID_ATTRIBUTE).setDataType(BOOL).setLabel("ID attribute")
.setValidationExpression(getIdAttributeValidationExpression());
addAttribute(IS_LABEL_ATTRIBUTE).setDataType(BOOL).setLabel("Label attribute");
addAttribute(LOOKUP_ATTRIBUTE_INDEX).setDataType(INT).setLabel("Lookup attribute index")
.setValidationExpression(getLookupAttributeValidationExpression());
Attribute parentAttr = addAttribute(PARENT).setDataType(XREF).setRefEntity(this).setLabel("Attribute parent");
addAttribute(CHILDREN).setDataType(ONE_TO_MANY).setRefEntity(this).setMappedBy(parentAttr)
.setOrderBy(new Sort(SEQUENCE_NR)).setLabel("Attribute parts");
addAttribute(REF_ENTITY_TYPE).setDataType(XREF).setRefEntity(entityTypeMeta).setLabel("Referenced entity")
.setValidationExpression(getRefEntityValidationExpression());
addAttribute(MAPPED_BY).setDataType(XREF).setRefEntity(this).setLabel("Mapped by").setDescription(
"Attribute in the referenced entity that owns the relationship of a onetomany attribute")
.setValidationExpression(getMappedByValidationExpression()).setReadOnly(true);
addAttribute(ORDER_BY).setLabel("Order by").setDescription(
"Order expression that defines entity collection order of a onetomany attribute (e.g. \"attr0\", \"attr0,ASC\", \"attr0,DESC\" or \"attr0,ASC;attr1,DESC\"")
.setValidationExpression(getOrderByValidationExpression());
addAttribute(EXPRESSION).setNillable(true).setLabel("Expression")
.setDescription("Computed value expression in Magma JavaScript");
addAttribute(IS_NULLABLE).setDataType(BOOL).setNillable(false).setLabel("Nillable").setDefaultValue("true");
addAttribute(IS_AUTO).setDataType(BOOL).setNillable(false).setLabel("Auto")
.setDescription("Auto generated values").setValidationExpression(getAutoValidationExpression());
addAttribute(IS_VISIBLE).setDataType(BOOL).setNillable(false).setLabel("Visible");
addAttribute(LABEL, ROLE_LOOKUP).setLabel("Label");
addAttribute(DESCRIPTION).setDataType(TEXT).setLabel("Description");
addAttribute(IS_AGGREGATABLE).setDataType(BOOL).setNillable(false).setLabel("Aggregatable")
.setValidationExpression(getAggregatableExpression());
addAttribute(ENUM_OPTIONS).setDataType(TEXT).setLabel("Enum values").setDescription("For data type ENUM")
.setValidationExpression(getEnumOptionsValidationExpression());
addAttribute(RANGE_MIN).setDataType(LONG).setLabel("Range min")
.setValidationExpression(getRangeValidationExpression(RANGE_MIN));
addAttribute(RANGE_MAX).setDataType(LONG).setLabel("Range max")
.setValidationExpression(getRangeValidationExpression(RANGE_MAX));
addAttribute(IS_READ_ONLY).setDataType(BOOL).setNillable(false).setLabel("Read-only");
addAttribute(IS_UNIQUE).setDataType(BOOL).setNillable(false).setLabel("Unique");
addAttribute(TAGS).setDataType(MREF).setRefEntity(tagMetadata).setLabel("Tags");
addAttribute(VISIBLE_EXPRESSION).setDataType(SCRIPT).setNillable(true).setLabel("Visible expression");
addAttribute(VALIDATION_EXPRESSION).setDataType(SCRIPT).setNillable(true).setLabel("Validation expression");
addAttribute(DEFAULT_VALUE).setDataType(TEXT).setNillable(true).setLabel("Default value");
}
// setter injection instead of constructor injection to avoid unresolvable circular dependencies
@Autowired
public void setTagMetadata(TagMetadata tagMetadata)
{
this.tagMetadata = requireNonNull(tagMetadata);
}
@Autowired
public void setEntityTypeMetadata(EntityTypeMetadata entityTypeMeta)
{
this.entityTypeMeta = requireNonNull(entityTypeMeta);
}
private static String getMappedByValidationExpression()
{
return "$('" + MAPPED_BY + "').isNull().and($('" + TYPE + "').eq('" + getValueString(ONE_TO_MANY)
+ "').not()).or(" + "$('" + MAPPED_BY + "').isNull().not().and($('" + TYPE + "').eq('" + getValueString(
ONE_TO_MANY) + "'))).value()";
}
private static String getOrderByValidationExpression()
{
String regex = "/^\\w+(,(ASC|DESC))?(;\\w+(,(ASC|DESC))?)*$/";
return "$('" + ORDER_BY + "').isNull().or(" + "$('" + ORDER_BY + "').matches(" + regex + ").and($('" + TYPE
+ "').eq('" + getValueString(ONE_TO_MANY) + "'))).value()";
}
private static String getEnumOptionsValidationExpression()
{
return "$('" + ENUM_OPTIONS + "').isNull().and($('" + TYPE + "').eq('" + getValueString(ENUM) + "').not()).or("
+ "$('" + ENUM_OPTIONS + "').isNull().not().and($('" + TYPE + "').eq('" + getValueString(ENUM)
+ "'))).value()";
}
private static String getRefEntityValidationExpression()
{
String regex = "/^(" + Arrays.stream(AttributeType.values()).filter(EntityTypeUtils::isReferenceType)
.map(AttributeType::getValueString).collect(Collectors.joining("|")) + ")$/";
return "$('" + REF_ENTITY_TYPE + "').isNull().and($('" + TYPE + "').matches(" + regex + ").not()).or(" + "$('"
+ REF_ENTITY_TYPE + "').isNull().not().and($('" + TYPE + "').matches(" + regex + "))).value()";
}
private static String getAutoValidationExpression()
{
String dateTypeRegex = "/^(" + Arrays.stream(AttributeType.values()).filter(EntityTypeUtils::isDateType)
.map(AttributeType::getValueString).collect(Collectors.joining("|")) + ")$/";
String autoIsTrue = "$('" + IS_AUTO + "').eq(true)";
String autoIsFalse = "$('" + IS_AUTO + "').eq(false)";
String autoIsTrueAndIsIdIsTrueAndTypeIsStringOrNull =
autoIsTrue + ".and($('" + IS_ID_ATTRIBUTE + "').eq(true).and($('" + TYPE + "').eq('" + getValueString(
STRING) + "').or($('" + TYPE + "').isNull())))";
String autoIsTrueAndIsIdIsFalseOrNullAndTypeIsDateType =
autoIsTrue + ".and($('" + IS_ID_ATTRIBUTE + "').eq(false).or($('" + IS_ID_ATTRIBUTE
+ "').isNull())).and($('" + TYPE + "').matches(" + dateTypeRegex + "))";
return autoIsFalse + ".or(" + autoIsTrueAndIsIdIsTrueAndTypeIsStringOrNull + ").or("
+ autoIsTrueAndIsIdIsFalseOrNullAndTypeIsDateType + ").value()";
}
private static String getRangeValidationExpression(String attribute)
{
String regex = "/^(" + Arrays.stream(AttributeType.values()).filter(EntityTypeUtils::isIntegerType)
.map(AttributeType::getValueString).collect(Collectors.joining("|")) + ")$/";
String rangeIsNull = "$('" + attribute + "').isNull()";
return rangeIsNull + ".or(" + rangeIsNull + ".not().and($('" + TYPE + "').matches(" + regex + "))).value()";
}
// Package private for testability
static String getIdAttributeValidationExpression()
{
String isIdIsTrue = "$('" + IS_ID_ATTRIBUTE + "').eq(true)";
String isIdIsFalseOrNull = "$('" + IS_ID_ATTRIBUTE + "').eq(false).or($('" + IS_ID_ATTRIBUTE + "').isNull())";
// Use the valid ID attribute types to constuct the validation expression
List<String> typeExpressions = getValidIdAttributeTypes().stream()
.map(attributeType -> "$('" + TYPE + "').eq('" + getValueString(attributeType) + "')")
.collect(Collectors.toList());
boolean first = true;
String typeIsNullOrStringOrIntOrLong = "";
for (String expression : typeExpressions)
{
if (first)
{
typeIsNullOrStringOrIntOrLong += expression;
first = false;
continue;
}
typeIsNullOrStringOrIntOrLong += ".or(" + expression + ")";
}
typeIsNullOrStringOrIntOrLong += ".or($('" + TYPE + "').isNull())";
String nullableIsFalse = "$('" + IS_NULLABLE + "').eq(false)";
return isIdIsFalseOrNull + ".or(" + isIdIsTrue + ".and(" + typeIsNullOrStringOrIntOrLong + ").and("
+ nullableIsFalse + ")).value()";
}
private static String getLookupAttributeValidationExpression()
{
String regex = "/^(" + Arrays.stream(AttributeType.values()).filter(EntityTypeUtils::isReferenceType)
.map(AttributeType::getValueString).collect(Collectors.joining("|")) + ")$/";
return "$('" + LOOKUP_ATTRIBUTE_INDEX + "').isNull().or(" + "$('" + LOOKUP_ATTRIBUTE_INDEX
+ "').isNull().not().and($('" + TYPE + "').matches(" + regex + ").not())).value()";
}
private static String getAggregatableExpression()
{
String aggregatableIsNullOrFalse =
"$('" + IS_AGGREGATABLE + "').isNull().or($('" + IS_AGGREGATABLE + "').eq(false))";
String regex = "/^(" + Arrays.stream(AttributeType.values()).filter(EntityTypeUtils::isReferenceType)
.map(AttributeType::getValueString).collect(Collectors.joining("|")) + ")$/";
return aggregatableIsNullOrFalse + ".or(" + "$('" + TYPE + "')" + ".matches(" + regex + ")" + ".and(" + "$('"
+ IS_NULLABLE + "')" + ".eq(false)" + ")" + ")" + ".or(" + "$('" + TYPE + "')" + ".matches(" + regex
+ ").not()).value()";
}
}