package org.molgenis.data.validation.meta;
import com.google.common.collect.Multimap;
import org.molgenis.data.DataService;
import org.molgenis.data.MolgenisDataException;
import org.molgenis.data.RepositoryCollection;
import org.molgenis.data.meta.MetaUtils;
import org.molgenis.data.meta.model.Attribute;
import org.molgenis.data.meta.model.EntityType;
import org.molgenis.data.meta.model.Package;
import org.molgenis.data.meta.system.SystemEntityTypeRegistry;
import org.molgenis.data.support.AttributeUtils;
import org.molgenis.data.validation.ConstraintViolation;
import org.molgenis.data.validation.MolgenisValidationException;
import org.molgenis.util.stream.MultimapCollectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.StreamSupport.stream;
import static org.molgenis.data.meta.NameValidator.validateName;
import static org.molgenis.data.meta.model.AttributeMetadata.ATTRIBUTE_META_DATA;
import static org.molgenis.data.meta.model.EntityTypeMetadata.ENTITY_TYPE_META_DATA;
import static org.molgenis.data.meta.model.PackageMetadata.PACKAGE;
import static org.molgenis.util.EntityUtils.asStream;
/**
* Entity metadata validator
*/
@Component
public class EntityTypeValidator
{
private final DataService dataService;
private final SystemEntityTypeRegistry systemEntityTypeRegistry;
@Autowired
public EntityTypeValidator(DataService dataService, SystemEntityTypeRegistry systemEntityTypeRegistry)
{
this.dataService = requireNonNull(dataService);
this.systemEntityTypeRegistry = requireNonNull(systemEntityTypeRegistry);
}
/**
* Validates entity meta data
*
* @param entityType entity meta data
* @throws MolgenisValidationException if entity meta data is not valid
*/
public void validate(EntityType entityType)
{
validateEntityName(entityType);
validatePackage(entityType);
validateExtends(entityType);
validateOwnAttributes(entityType);
Map<String, Attribute> ownAllAttrMap = stream(entityType.getOwnAllAttributes().spliterator(), false)
.collect(toMap(Attribute::getIdentifier, Function.identity(), (u, v) ->
{
throw new IllegalStateException(String.format("Duplicate key %s", u));
}, LinkedHashMap::new));
validateOwnIdAttribute(entityType, ownAllAttrMap);
validateOwnLabelAttribute(entityType, ownAllAttrMap);
validateOwnLookupAttributes(entityType, ownAllAttrMap);
validateBackend(entityType);
}
/**
* Validate that the entity meta data backend exists
*
* @param entityType entity meta data
* @throws MolgenisValidationException if the entity meta data backend does not exist
*/
private void validateBackend(EntityType entityType)
{
// Validate backend exists
String backendName = entityType.getBackend();
RepositoryCollection repoCollection = dataService.getMeta().getBackend(backendName);
if (repoCollection == null)
{
throw new MolgenisValidationException(new ConstraintViolation(format("Unknown backend [%s]", backendName)));
}
}
/**
* Validate that the lookup attributes owned by this entity are part of the owned attributes.
*
* @param entityType entity meta data
* @param ownAllAttrMap attribute identifier to attribute map
* @throws MolgenisValidationException if one or more lookup attributes are not entity attributes
*/
private static void validateOwnLookupAttributes(EntityType entityType, Map<String, Attribute> ownAllAttrMap)
{
// Validate lookup attributes
entityType.getOwnLookupAttributes().forEach(ownLookupAttr ->
{
// Validate that lookup attribute is in the attributes list
Attribute ownAttr = ownAllAttrMap.get(ownLookupAttr.getIdentifier());
if (ownAttr == null)
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Lookup attribute [%s] is not part of the entity attributes", ownLookupAttr.getName())));
}
});
}
/**
* Validate that the label attribute owned by this entity is part of the owned attributes.
*
* @param entityType entity meta data
* @param ownAllAttrMap attribute identifier to attribute map
* @throws MolgenisValidationException if the label attribute is not an entity attribute
*/
private static void validateOwnLabelAttribute(EntityType entityType, Map<String, Attribute> ownAllAttrMap)
{
// Validate label attribute
Attribute ownLabelAttr = entityType.getOwnLabelAttribute();
if (ownLabelAttr != null)
{
// Validate that label attribute is in the attributes list
Attribute ownAttr = ownAllAttrMap.get(ownLabelAttr.getIdentifier());
if (ownAttr == null)
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Label attribute [%s] is not part of the entity attributes", ownLabelAttr.getName())));
}
}
}
/**
* Validate that the ID attribute owned by this entity is part of the owned attributes.
*
* @param entityType entity meta data
* @param ownAllAttrMap attribute identifier to attribute map
* @throws MolgenisValidationException if the ID attribute is not an entity attribute
*/
private static void validateOwnIdAttribute(EntityType entityType, Map<String, Attribute> ownAllAttrMap)
{
// Validate ID attribute
Attribute ownIdAttr = entityType.getOwnIdAttribute();
if (ownIdAttr != null)
{
// Validate that ID attribute is in the attributes list
Attribute ownAttr = ownAllAttrMap.get(ownIdAttr.getIdentifier());
if (ownAttr == null)
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] ID attribute [%s] is not part of the entity attributes",
entityType.getName(), ownIdAttr.getName())));
}
// Validate that ID attribute data type is allowed
if (!AttributeUtils.isIdAttributeTypeAllowed(ownIdAttr))
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] ID attribute [%s] type [%s] is not allowed", entityType.getName(),
ownIdAttr.getName(), ownIdAttr.getDataType().toString())));
}
// Validate that ID attribute is unique
if (!ownIdAttr.isUnique())
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] ID attribute [%s] is not a unique attribute", entityType.getName(),
ownIdAttr.getName())));
}
// Validate that ID attribute is not nillable
if (ownIdAttr.isNillable())
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] ID attribute [%s] is not a non-nillable attribute", entityType.getName(),
ownIdAttr.getName())));
}
}
else
{
if (!entityType.isAbstract() && entityType.getIdAttribute() == null)
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] is missing required ID attribute", entityType.getName())));
}
}
}
/**
* Validates the attributes owned by this entity:
* 1) validates that the parent entity doesn't have attributes with the same name
* 2) validates that this entity doesn't have attributes with the same name
*
* @param entityType entity meta data
* @throws MolgenisValidationException if an attribute is owned by another entity or a parent attribute has the same name
*/
private static void validateOwnAttributes(EntityType entityType)
{
// Validate that entity does not contain multiple attributes with the same name
Multimap<String, Attribute> attrMultiMap = asStream(entityType.getAllAttributes())
.collect(MultimapCollectors.toArrayListMultimap(Attribute::getName, Function.identity()));
attrMultiMap.keySet().forEach(attrName ->
{
if (attrMultiMap.get(attrName).size() > 1)
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Entity [%s] contains multiple attributes with name [%s]", entityType.getName(),
attrName)));
}
});
// Validate that entity attributes with same name do no exist in parent entity
EntityType extendsEntityType = entityType.getExtends();
if (extendsEntityType != null)
{
Map<String, Attribute> extendsAllAttrMap = stream(extendsEntityType.getAllAttributes().spliterator(), false)
.collect(toMap(Attribute::getName, Function.identity(), (u, v) ->
{
throw new IllegalStateException(String.format("Duplicate key %s", u));
}, LinkedHashMap::new));
entityType.getOwnAllAttributes().forEach(attr ->
{
if (extendsAllAttrMap.containsKey(attr.getName()))
{
throw new MolgenisValidationException(new ConstraintViolation(
format("An attribute with name [%s] already exists in entity [%s] or one of its parents",
attr.getName(), extendsEntityType.getName())));
}
});
}
}
/**
* Validates if this entityType extends another entityType. If so, checks whether that parent entityType is abstract.
*
* @param entityType entity meta data
* @throws MolgenisValidationException if the entity extends from a non-abstract entity
*/
private static void validateExtends(EntityType entityType)
{
if (entityType.getExtends() != null)
{
EntityType extendedEntityType = entityType.getExtends();
if (!extendedEntityType.isAbstract())
{
throw new MolgenisValidationException(new ConstraintViolation(
format("EntityType [%s] is not abstract; EntityType [%s] can't extend it",
entityType.getExtends().getName(), entityType.getName())));
}
}
}
/**
* Validates the entity fully qualified name and simple name:
* - Validates that the entity simple name does not contain illegal characters and validates the name length
* - Validates that the fully qualified name, simple name and package name are consistent with each other
*
* @param entityType entity meta data
* @throws MolgenisValidationException if the entity simple name content is invalid or the fully qualified name, simple name and package name are not consistent
*/
private static void validateEntityName(EntityType entityType)
{
// validate entity name (e.g. illegal characters, length)
String name = entityType.getName();
if (!name.equals(ATTRIBUTE_META_DATA) && !name.equals(ENTITY_TYPE_META_DATA) && !name.equals(PACKAGE))
{
try
{
validateName(entityType.getSimpleName());
}
catch (MolgenisDataException e)
{
throw new MolgenisValidationException(new ConstraintViolation(e.getMessage()));
}
}
// Validate that entity name equals entity package name + package separator + entity simple name
Package package_ = entityType.getPackage();
if (package_ != null)
{
if (!(package_.getName() + Package.PACKAGE_SEPARATOR + entityType.getSimpleName()).equals(entityType.getName()))
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Qualified entity name [%s] not equal to entity package name [%s] underscore entity name [%s]",
entityType.getName(), package_.getName(), entityType.getSimpleName())));
}
}
else
{
if (!entityType.getSimpleName().equals(entityType.getName()))
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Qualified entity name [%s] not equal to entity name [%s]", entityType.getName(),
entityType.getSimpleName())));
}
}
}
/**
* Validate that non-system entities are not assigned to a system package
*
* @param entityType entity type
*/
private void validatePackage(EntityType entityType)
{
Package package_ = entityType.getPackage();
if (package_ != null)
{
if (MetaUtils.isSystemPackage(package_) && !systemEntityTypeRegistry
.hasSystemEntityType(entityType.getName()))
{
throw new MolgenisValidationException(new ConstraintViolation(
format("Adding entity [%s] to system package [%s] is not allowed", entityType.getName(),
entityType.getPackage().getName())));
}
}
}
}