package org.molgenis.data.meta.model; import com.google.common.collect.Maps; import org.molgenis.data.Entity; import org.molgenis.data.MolgenisDataException; import org.molgenis.data.support.StaticEntity; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.removeAll; import static com.google.common.collect.Lists.newArrayList; import static java.lang.String.format; import static java.util.Collections.singletonList; import static java.util.Collections.sort; import static java.util.stream.Collectors.*; import static java.util.stream.StreamSupport.stream; import static org.molgenis.data.meta.AttributeType.COMPOUND; import static org.molgenis.data.meta.model.AttributeMetadata.DESCRIPTION; import static org.molgenis.data.meta.model.AttributeMetadata.LABEL; import static org.molgenis.data.meta.model.EntityType.AttributeCopyMode.DEEP_COPY_ATTRS; import static org.molgenis.data.meta.model.EntityType.AttributeCopyMode.SHALLOW_COPY_ATTRS; import static org.molgenis.data.meta.model.EntityTypeMetadata.*; import static org.molgenis.data.meta.model.Package.PACKAGE_SEPARATOR; import static org.molgenis.data.support.AttributeUtils.getI18nAttributeName; /** * EntityType defines the structure and attributes of an Entity. Attributes are unique. Other software components * can use this to interact with Entity and/or to configure backends and frontends, including Repository instances. */ public class EntityType extends StaticEntity { private transient Map<String, Attribute> cachedOwnAttrs; private transient Boolean cachedHasAttrWithExpession; public EntityType(Entity entity) { super(entity); } /** * Creates a new entity meta data. */ protected EntityType() { } /** * Creates a new entity meta data. Normally called by its {@link EntityTypeFactory entity factory}. * * @param entityType entity meta data */ public EntityType(EntityType entityType) { super(entityType); setDefaultValues(); } /** * Creates a new entity meta data with the given identifier. Normally called by its {@link EntityTypeFactory entity factory}. * * @param entityId entity identifier (fully qualified entity name) * @param entityType entity meta data */ public EntityType(String entityId, EntityType entityType) { super(entityType); setDefaultValues(); //FIXME: This is incorrect, the ID value is the fully qualified name, not the simple name! setSimpleName(entityId); } public enum AttributeCopyMode { SHALLOW_COPY_ATTRS, DEEP_COPY_ATTRS } /** * Copy-factory (instead of copy-constructor to avoid accidental method overloading to * {@link #EntityType(EntityType)}). Creates shallow-copy of package, tags and extended entity. * * @param entityType entity meta data * @return copy of entity meta data */ public static EntityType newInstance(EntityType entityType) { return newInstance(entityType, SHALLOW_COPY_ATTRS, null); } /** * Copy-factory (instead of copy-constructor to avoid accidental method overloading to * {@link #EntityType(EntityType)}). Creates shallow-copy of package, tags and extended entity. * * @param entityType entity meta data * @param attrCopyMode attribute copy mode that defines whether to deep-copy or shallow-copy attributes * @param attrFactory attribute factory used to create new attributes in deep-copy mode * @return copy of entity meta data */ public static EntityType newInstance(EntityType entityType, AttributeCopyMode attrCopyMode, AttributeFactory attrFactory) { EntityType entityTypeCopy = new EntityType(entityType.getEntityType()); // do not deep-copy entityTypeCopy.setSimpleName(entityType.getSimpleName()); entityTypeCopy.setPackage(entityType.getPackage()); // do not deep-copy entityTypeCopy.setLabel(entityType.getLabel()); entityTypeCopy.setDescription(entityType.getDescription()); // Own attributes (deep copy or shallow copy) if (attrCopyMode == DEEP_COPY_ATTRS) { // step #1: deep copy attributes LinkedHashMap<String, Attribute> ownAttrMap = stream(entityType.getOwnAllAttributes().spliterator(), false) .map(attr -> Attribute.newInstance(attr, attrCopyMode, attrFactory)) .map(attrCopy -> attrCopy.setEntity(entityTypeCopy)) .collect(toMap(Attribute::getName, Function.identity(), (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, LinkedHashMap::new)); // step #2: update attribute.parent relations ownAttrMap.forEach((attrName, ownAttr) -> { Attribute ownAttrParent = ownAttr.getParent(); if (ownAttrParent != null) { ownAttr.setParent(ownAttrMap.get(ownAttrParent.getName())); } }); entityTypeCopy.setOwnAllAttributes(ownAttrMap.values()); } else { entityTypeCopy.setOwnAllAttributes(newArrayList(entityType.getOwnAllAttributes())); } entityTypeCopy.setAbstract(entityType.isAbstract()); entityTypeCopy.setExtends(entityType.getExtends()); // do not deep-copy entityTypeCopy.setTags(newArrayList(entityType.getTags())); // do not deep-copy entityTypeCopy.setBackend(entityType.getBackend()); return entityTypeCopy; } @Override public Iterable<String> getAttributeNames() { return stream(getEntities(ATTRIBUTES, Attribute.class).spliterator(), false).map(Attribute::getName)::iterator; } /** * Gets the fully qualified entity name. * * @return fully qualified entity name */ public String getName() { return getString(FULL_NAME); } /** * Sets the fully qualified entity name. * In case this entity simple name is null, assigns the fully qualified entity name to the simple name. * * @param fullName fully qualified entity name. * @return this entity meta data for chaining */ public EntityType setName(String fullName) { set(FULL_NAME, fullName); if (getSimpleName() == null) { set(SIMPLE_NAME, fullName); } if (getLabel() == null) { set(LABEL, fullName); } return this; } /** * Gets the entity name. * * @return entity name */ public String getSimpleName() { return getString(SIMPLE_NAME); } /** * Sets the entity name. * In case this entity label is null, assigns the entity name to the label. * * @param simpleName entity name. * @return this entity meta data for chaining */ public EntityType setSimpleName(String simpleName) { set(SIMPLE_NAME, simpleName); updateFullName(); if (getLabel() == null) { setLabel(simpleName); } return this; } /** * Human readable entity label * * @return entity label */ public String getLabel() { return getString(LABEL); } /** * Label of the entity in the requested language * * @return entity label */ public String getLabel(String languageCode) { String i18nLabel = getString(getI18nAttributeName(LABEL, languageCode)); return i18nLabel != null ? i18nLabel : getLabel(); } public EntityType setLabel(String label) { if (label == null) { label = getSimpleName(); } set(LABEL, label); return this; } public EntityType setLabel(String languageCode, String label) { set(getI18nAttributeName(LABEL, languageCode), label); return this; } /** * Description of the entity * * @return entity description */ public String getDescription() { return getString(DESCRIPTION); } /** * Description of the entity in the requested language * * @return entity description */ public String getDescription(String languageCode) { String i18nDescription = getString(getI18nAttributeName(DESCRIPTION, languageCode)); return i18nDescription != null ? i18nDescription : getDescription(); } public EntityType setDescription(String description) { set(DESCRIPTION, description); return this; } public EntityType setDescription(String languageCode, String description) { set(getI18nAttributeName(DESCRIPTION, languageCode), description); return this; } /** * The name of the repostory collection/backend where the entities of this type are stored * * @return backend name */ public String getBackend() { return getString(BACKEND); } public EntityType setBackend(String backend) { set(BACKEND, backend); return this; } /** * Gets the package where this entity belongs to * * @return package */ public Package getPackage() { return getEntity(PACKAGE, Package.class); } public EntityType setPackage(Package package_) { set(PACKAGE, package_); updateFullName(); return this; } /** * Attribute that is used as unique Id. Id attribute should always be provided. * * @return id attribute */ public Attribute getIdAttribute() { Attribute idAttr = getOwnIdAttribute(); if (idAttr == null) { EntityType extends_ = getExtends(); if (extends_ != null) { idAttr = extends_.getIdAttribute(); } } return idAttr; } /** * Same as {@link #getIdAttribute()} but returns null if the id attribute is defined in its parent class. * * @return id attribute */ public Attribute getOwnIdAttribute() { for (Attribute ownAttr : getOwnAllAttributes()) { if (ownAttr.isIdAttribute()) { return ownAttr; } } return null; } /** * Attribute that is used as unique label. If no label exist, returns getIdAttribute(). * * @return label attribute */ public Attribute getLabelAttribute() { Attribute labelAttr = getOwnLabelAttribute(); if (labelAttr == null) { EntityType extends_ = getExtends(); if (extends_ != null) { labelAttr = extends_.getLabelAttribute(); } } return labelAttr; } /** * Gets the correct label attribute for the given language, or the default if not found * * @param langCode language code * @return label attribute */ public Attribute getLabelAttribute(String langCode) { Attribute labelAttr = getLabelAttribute(); Attribute i18nLabelAttr = labelAttr != null ? getAttribute(labelAttr.getName() + '-' + langCode) : null; return i18nLabelAttr != null ? i18nLabelAttr : labelAttr; } /** * Same as {@link #getLabelAttribute()} but returns null if the label does not exist or the label exists in its * parent class. * * @return label attribute */ // FIXME cache own label attribute public Attribute getOwnLabelAttribute() { for (Attribute ownAttr : getOwnAllAttributes()) { if (ownAttr.isLabelAttribute()) { return ownAttr; } } return null; } public Attribute getOwnLabelAttribute(String languageCode) { Attribute labelAttr = getOwnLabelAttribute(); if (labelAttr != null) { return getEntity(getI18nAttributeName(labelAttr.getName(), languageCode), Attribute.class); } else { return null; } } /** * Get lookup attribute by name (case insensitive), returns null if not found * * @param lookupAttrName lookup attribute name * @return lookup attribute or <tt>null</tt> */ public Attribute getLookupAttribute(String lookupAttrName) { return stream(getLookupAttributes().spliterator(), false) .filter(lookupAttr -> lookupAttr.getName().equals(lookupAttrName)).findFirst().orElse(null); } /** * Returns attributes that must be searched in case of xref/mref search * * @return lookup attributes */ public Iterable<Attribute> getLookupAttributes() { Iterable<Attribute> lookupAttributes = getOwnLookupAttributes(); EntityType extends_ = getExtends(); if (extends_ != null) { lookupAttributes = concat(lookupAttributes, extends_.getLookupAttributes()); } return lookupAttributes; } /** * Returns attributes that must be searched in case of xref/mref search * * @return lookup attributes */ public Iterable<Attribute> getOwnLookupAttributes() { List<Attribute> ownLookupAttrs = stream(getOwnAllAttributes().spliterator(), false) .filter(attr -> attr.getLookupAttributeIndex() != null).collect(toCollection(ArrayList::new)); if (ownLookupAttrs.size() > 1) { sort(ownLookupAttrs, (o1, o2) -> o1.getLookupAttributeIndex() < o2.getLookupAttributeIndex() ? -1 : 1); } return ownLookupAttrs; } /** * Entities can be abstract (analogous an 'interface' or 'protocol'). Use is to define reusable Entity model * components that cannot be instantiated themselves (i.e. there cannot be data attached to this entity meta data). * * @return whether or not this entity is an abstract entity */ public boolean isAbstract() { Boolean abstract_ = getBoolean(IS_ABSTRACT); return abstract_ != null ? abstract_ : false; } public EntityType setAbstract(boolean abstract_) { set(IS_ABSTRACT, abstract_); return this; } /** * Entity can extend another entity, adding its properties to their own * * @return parent entity */ public EntityType getExtends() { return getEntity(EXTENDS, EntityType.class); } public EntityType setExtends(EntityType extends_) { set(EXTENDS, extends_); return this; } /** * Same as {@link #getAttributes()} but does not return attributes of its parent class. * * @return entity attributes without extended entity attributes */ public Iterable<Attribute> getOwnAttributes() { return stream(getOwnAllAttributes().spliterator(), false).filter(attr -> attr.getParent() == null) .collect(toList()); } public EntityType setOwnAllAttributes(Iterable<Attribute> attrs) { invalidateCachedOwnAttrs(); set(ATTRIBUTES, attrs); return this; } // FIXME add getter/setter for tags /** * Returns all attributes. In case of compound attributes (attributes consisting of atomic attributes) only the * compound attribute is returned. This attribute can be used to retrieve parts of the compound attribute. * <p> * In case EntityType extends other EntityType then the attributes of this EntityType as well as its * parent class are returned. * * @return entity attributes */ public Iterable<Attribute> getAttributes() { Iterable<Attribute> attrs = getOwnAttributes(); EntityType extends_ = getExtends(); if (extends_ != null) { attrs = concat(attrs, extends_.getAttributes()); } return attrs; } /** * Returns all atomic attributes. In case of compound attributes (attributes consisting of atomic attributes) only * the descendant atomic attributes are returned. The compound attribute itself is not returned. * <p> * In case EntityType extends other EntityType then the attributes of this EntityType as well as its * parent class are returned. * * @return atomic attributes */ public Iterable<Attribute> getAtomicAttributes() { Iterable<Attribute> atomicAttrs = getOwnAtomicAttributes(); EntityType extends_ = getExtends(); if (extends_ != null) { atomicAttrs = concat(atomicAttrs, extends_.getAtomicAttributes()); } return atomicAttrs; } public Iterable<Attribute> getAllAttributes() { Iterable<Attribute> allAttrs = getOwnAllAttributes(); EntityType extends_ = getExtends(); if (extends_ != null) { allAttrs = concat(allAttrs, extends_.getAllAttributes()); } return allAttrs; } public Iterable<Attribute> getOwnAllAttributes() { return getCachedOwnAttrs().values(); } /** * Get attribute by name * * @return attribute or <tt>null</tt> */ public Attribute getAttribute(String attrName) { Attribute attr = getCachedOwnAttrs().get(attrName); if (attr == null) { // look up attribute in parent entity EntityType extendsEntityType = getExtends(); if (extendsEntityType != null) { attr = extendsEntityType.getAttribute(attrName); } } return attr; } public EntityType addAttribute(Attribute attr, AttributeRole... attrTypes) { invalidateCachedOwnAttrs(); Iterable<Attribute> attrs = getEntities(ATTRIBUTES, Attribute.class); // validate that no other attribute exists with the same name attrs.forEach(existingAttr -> { if (existingAttr.getName().equals(attr.getName())) { throw new MolgenisDataException( format("Entity [%s] already contains attribute with name [%s], duplicate attribute names are not allowed", this.getName(), attr.getName())); } }); attr.setEntity(this); this.addSequenceNumber(attr, attrs); set(ATTRIBUTES, concat(attrs, singletonList(attr))); setAttributeRoles(attr, attrTypes); return this; } /** * Add a sequence number to the attribute. * If the sequence number exists add it ot the attribute. * If the sequence number does not exists then find the highest sequence number. * If Entity has not attributes with sequence numbers put 0. * * @param attr the attribute to add * @param attrs existing attributes */ static void addSequenceNumber(Attribute attr, Iterable<Attribute> attrs) { Integer sequenceNumber = attr.getSequenceNumber(); if (null == sequenceNumber) { int i = StreamSupport.stream(attrs.spliterator(), false).filter(a -> null != a.getSequenceNumber()) .mapToInt(a -> a.getSequenceNumber()).max().orElse(-1); if (i == -1) attr.setSequenceNumber(0); else attr.setSequenceNumber(++i); } } public void addAttributes(Iterable<Attribute> attrs) { attrs.forEach(this::addAttribute); } protected void setAttributeRoles(Attribute attr, AttributeRole... attrTypes) { if (attrTypes != null) { for (AttributeRole attrType : attrTypes) { switch (attrType) { case ROLE_ID: attr.setIdAttribute(true); if (getLabelAttribute() == null) { attr.setLabelAttribute(true); } break; case ROLE_LABEL: Attribute currentLabelAttr = getLabelAttribute(); if (currentLabelAttr != null) { currentLabelAttr.setLabelAttribute(false); } attr.setLabelAttribute(true); break; case ROLE_LOOKUP: attr.setLookupAttributeIndex(0); // FIXME assign unique lookup attribute index break; default: throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString())); } } } } /** * Returns whether this entity has an attribute with expression * * @return whether this entity has an attribute with expression */ public boolean hasAttributeWithExpression() { return getCachedHasAttrWithExpession(); } private boolean getCachedHasAttrWithExpession() { if (cachedHasAttrWithExpession == null) { cachedHasAttrWithExpession = stream(getAtomicAttributes().spliterator(), false) .anyMatch(attr -> attr.getExpression() != null); } return cachedHasAttrWithExpession; } public void removeAttribute(Attribute attr) { Map<String, Attribute> cachedOwnAttrs = getCachedOwnAttrs(); cachedOwnAttrs.remove(attr.getName()); set(ATTRIBUTES, cachedOwnAttrs.values()); } /** * Get all tags for this entity * * @return entity tags */ public Iterable<Tag> getTags() { return getEntities(TAGS, Tag.class); } /** * Set tags for this entity * * @param tags entity tags * @return this entity */ public EntityType setTags(Iterable<Tag> tags) { set(TAGS, tags); return this; } /** * Add a tag for this entity * * @param tag entity tag */ public void addTag(Tag tag) { set(TAGS, concat(getTags(), singletonList(tag))); } /** * Add a tag for this entity * * @param tag entity tag */ public void removeTag(Tag tag) { Iterable<Tag> tags = getTags(); removeAll(tags, singletonList(tag)); set(TAGS, tag); } /** * Returns all atomic attributes. In case of compound attributes (attributes consisting of atomic attributes) only * the descendant atomic attributes are returned. The compound attribute itself is not returned. * <p> * In case EntityType extends other EntityType then the attributes of this EntityType as well as its * parent class are returned. * * @return atomic attributes without extended entity atomic attributes */ public Iterable<Attribute> getOwnAtomicAttributes() { return () -> getCachedOwnAttrs().values().stream().filter(attr -> attr.getDataType() != COMPOUND).iterator(); } public boolean hasBidirectionalAttributes() { return hasMappedByAttributes() || hasInversedByAttributes(); } public boolean hasMappedByAttributes() { return getMappedByAttributes().findFirst().orElse(null) != null; } public Stream<Attribute> getOwnMappedByAttributes() { return stream(getOwnAtomicAttributes().spliterator(), false).filter(Attribute::isMappedBy); } public Stream<Attribute> getMappedByAttributes() { return stream(getAtomicAttributes().spliterator(), false).filter(Attribute::isMappedBy); } public boolean hasInversedByAttributes() { return getInversedByAttributes().findFirst().orElse(null) != null; } public Stream<Attribute> getInversedByAttributes() { return stream(getAtomicAttributes().spliterator(), false).filter(Attribute::isInversedBy); } @Override public void set(String attributeName, Object value) { super.set(attributeName, value); switch (attributeName) { case ATTRIBUTES: invalidateCachedOwnAttrs(); break; default: break; } } private void updateFullName() { String simpleName = getSimpleName(); if (simpleName != null) { String fullName; Package package_ = getPackage(); if (package_ != null) { fullName = package_.getName() + PACKAGE_SEPARATOR + simpleName; } else { fullName = simpleName; } set(FULL_NAME, fullName); } } protected void setDefaultValues() { setAbstract(false); } private Map<String, Attribute> getCachedOwnAttrs() { if (cachedOwnAttrs == null) { cachedOwnAttrs = Maps.newLinkedHashMap(); getEntities(ATTRIBUTES, Attribute.class).forEach(attr -> cachedOwnAttrs.put(attr.getName(), attr)); } return cachedOwnAttrs; } private void invalidateCachedOwnAttrs() { cachedOwnAttrs = null; } public enum AttributeRole { ROLE_ID, ROLE_LABEL, ROLE_LOOKUP } @Override public String toString() { return "EntityType{" + "name=" + getName() + '}'; } }