package org.molgenis.data.meta.model; import com.google.common.collect.Lists; import org.molgenis.data.Entity; import org.molgenis.data.Range; import org.molgenis.data.Sort; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.support.StaticEntity; import java.util.List; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.removeAll; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; import static org.molgenis.data.meta.AttributeType.STRING; import static org.molgenis.data.meta.model.AttributeMetadata.*; import static org.molgenis.data.meta.model.EntityType.AttributeCopyMode.DEEP_COPY_ATTRS; import static org.molgenis.data.support.AttributeUtils.getI18nAttributeName; import static org.molgenis.data.support.EntityTypeUtils.isReferenceType; /** * Attribute defines the properties of an entity. Synonyms: feature, column, data item. */ public class Attribute extends StaticEntity { private transient AttributeType cachedDataType; public Attribute(Entity entity) { super(entity); } /** * Creates a new attribute. Normally called by its {@link AttributeFactory entity factory}. * * @param entityType attribute meta data */ public Attribute(EntityType entityType) { super(entityType); setDefaultValues(); } /** * Creates a new attribute with the given identifier. Normally called by its {@link AttributeFactory entity factory}. * * @param attrId attribute identifier (not the attribute name) * @param entityType attribute meta data */ public Attribute(String attrId, EntityType entityType) { super(entityType); setDefaultValues(); setIdentifier(attrId); } /** * Copy-factory (instead of copy-constructor to avoid accidental method overloading to * {@link #Attribute(EntityType)}). Creates a copy of attribute with a shallow copy of referenced * entity and tags. * * @param attrMeta attribute * @param attrCopyMode attribute copy mode that defines whether to deep-copy or shallow-copy attribute parts * @param attrFactory attribute factory used to create new attributes in deep-copy mode * @return shallow or deep copy of attribute */ public static Attribute newInstance(Attribute attrMeta, AttributeCopyMode attrCopyMode, AttributeFactory attrFactory) { Attribute attrMetaCopy = attrFactory.create(); // create new attribute with unique identifier attrMetaCopy.setName(attrMeta.getName()); attrMetaCopy.setEntity(attrMeta.getEntity()); attrMetaCopy.setSequenceNumber(attrMeta.getSequenceNumber()); attrMetaCopy.setDataType(attrMeta.getDataType()); attrMetaCopy.setIdAttribute(attrMeta.isIdAttribute()); attrMetaCopy.setLabelAttribute(attrMeta.isLabelAttribute()); attrMetaCopy.setLookupAttributeIndex(attrMeta.getLookupAttributeIndex()); attrMetaCopy.setRefEntity(attrMeta.getRefEntity()); // do not deep-copy attrMetaCopy.setMappedBy(attrMeta.getMappedBy()); // do not deep-copy attrMetaCopy.setOrderBy(attrMeta.getOrderBy()); attrMetaCopy.setExpression(attrMeta.getExpression()); attrMetaCopy.setNillable(attrMeta.isNillable()); attrMetaCopy.setAuto(attrMeta.isAuto()); attrMetaCopy.setLabel(attrMeta.getLabel()); attrMetaCopy.setDescription(attrMeta.getDescription()); attrMetaCopy.setAggregatable(attrMeta.isAggregatable()); attrMetaCopy.setEnumOptions(attrMeta.getEnumOptions()); attrMetaCopy.setRangeMin(attrMeta.getRangeMin()); attrMetaCopy.setRangeMax(attrMeta.getRangeMax()); attrMetaCopy.setReadOnly(attrMeta.isReadOnly()); attrMetaCopy.setUnique(attrMeta.isUnique()); Attribute parentAttr = attrMeta.getParent(); if (attrCopyMode == DEEP_COPY_ATTRS) { attrMetaCopy.setParent( parentAttr != null ? Attribute.newInstance(parentAttr, attrCopyMode, attrFactory) : null); } else { attrMetaCopy.setParent(parentAttr); } attrMetaCopy.setTags(Lists.newArrayList(attrMeta.getTags())); // do not deep-copy attrMetaCopy.setVisibleExpression(attrMeta.getVisibleExpression()); attrMetaCopy.setDefaultValue(attrMeta.getDefaultValue()); return attrMetaCopy; } public String getIdentifier() { return getString(ID); } public Attribute setIdentifier(String identifier) { set(ID, identifier); return this; } /** * Name of the attribute * * @return attribute name */ public String getName() { return getString(NAME); } public Attribute setName(String name) { set(NAME, name); return this; } /** * Attribute sequence number that determines attribute order within an entity * * @return attribute sequence number */ public Integer getSequenceNumber() { return getInt(SEQUENCE_NR); } public Attribute setSequenceNumber(int seqNr) { set(SEQUENCE_NR, seqNr); return this; } public EntityType getEntity() { return getEntity(ENTITY, EntityType.class); } public Attribute setEntity(EntityType entityMeta) { set(ENTITY, entityMeta); return this; } public boolean isIdAttribute() { Boolean isIdAttr = getBoolean(IS_ID_ATTRIBUTE); return isIdAttr != null && isIdAttr; } public Attribute setIdAttribute(Boolean isIdAttr) { set(IS_ID_ATTRIBUTE, isIdAttr); if (isIdAttr != null && isIdAttr) { setReadOnly(true); setUnique(true); setNillable(false); } return this; } public boolean isLabelAttribute() { Boolean isLabelAttr = getBoolean(IS_LABEL_ATTRIBUTE); return isLabelAttr != null && isLabelAttr; } public Attribute setLabelAttribute(Boolean isLabelAttr) { set(IS_LABEL_ATTRIBUTE, isLabelAttr); return this; } public Integer getLookupAttributeIndex() { return getInt(LOOKUP_ATTRIBUTE_INDEX); } public Attribute setLookupAttributeIndex(Integer lookupAttrIdx) { set(LOOKUP_ATTRIBUTE_INDEX, lookupAttrIdx); return this; } /** * Label of the attribute in the default language if set else returns name * * @return attribute label */ public String getLabel() { String label = getString(LABEL); return label != null ? label : getName(); } /** * Label of the attribute in the default language if set else returns name * * @return attribute label */ public String getLabel(String languageCode) { String i18nString = getString(getI18nAttributeName(LABEL, languageCode)); return i18nString != null ? i18nString : getLabel(); } public Attribute setLabel(String label) { set(LABEL, label); return this; } public Attribute setLabel(String languageCode, String label) { set(getI18nAttributeName(LABEL, languageCode), label); return this; } /** * Description of the attribute * * @return attribute description or <tt>null</tt> */ public String getDescription() { return getString(DESCRIPTION); } /** * Description of the attribute in the requested languages * * @return attribute description or <tt>null</tt> */ public String getDescription(String languageCode) { String i18nDescription = getString(getI18nAttributeName(DESCRIPTION, languageCode)); return i18nDescription != null ? i18nDescription : getDescription(); } public Attribute setDescription(String description) { set(DESCRIPTION, description); return this; } public Attribute setDescription(String languageCode, String description) { set(getI18nAttributeName(DESCRIPTION, languageCode), description); return this; } /** * Data type of the attribute * * @return attribute data type */ public AttributeType getDataType() { return getCachedDataType(); } public Attribute setDataType(AttributeType dataType) { invalidateCachedDataType(); set(TYPE, AttributeType.getValueString(dataType)); return this; } /** * When getDataType=compound, get compound attribute parts * * @return Iterable of attributes or empty Iterable if no attribute parts exist */ public Iterable<Attribute> getChildren() { return getEntities(CHILDREN, Attribute.class); } /** * When getDataType=xref/mref, get other end of xref * * @return referenced entity */ public EntityType getRefEntity() { return getEntity(REF_ENTITY_TYPE, EntityType.class); } public Attribute setRefEntity(EntityType refEntity) { set(REF_ENTITY_TYPE, refEntity); return this; } public Attribute getMappedBy() { return getEntity(MAPPED_BY, Attribute.class); } public Attribute setMappedBy(Attribute mappedByAttr) { set(MAPPED_BY, mappedByAttr); return this; } /** * Indicates if this attribute is the one-to-many back-reference of a bidirectionally navigable relationship. */ public boolean isMappedBy() { return getMappedBy() != null; } public Sort getOrderBy() { String orderByStr = getString(ORDER_BY); return orderByStr != null ? Sort.parse(orderByStr) : null; } public Attribute setOrderBy(Sort sort) { String orderByStr = sort != null ? sort.toSortString() : null; set(ORDER_BY, orderByStr); return this; } /** * Expression used to compute this attribute. * * @return String representation of expression, in JSON format */ public String getExpression() { return getString(EXPRESSION); } public Attribute setExpression(String expression) { set(EXPRESSION, expression); return this; } /** * Wheter attribute has an expression or not * * @return true if attribute has expression */ public boolean hasExpression() { return getExpression() != null; } /** * Whether attribute has not null constraint * * @return <tt>true</tt> if this attribute is nillable */ public boolean isNillable() { return requireNonNull(getBoolean(IS_NULLABLE)); } public Attribute setNillable(boolean nillable) { set(IS_NULLABLE, nillable); return this; } /** * When true the attribute is automatically assigned a value when persisted (for example the current date) * * @return <tt>true</tt> if this attribute is automatically assigned */ public boolean isAuto() { return requireNonNull(getBoolean(IS_AUTO)); } public Attribute setAuto(boolean auto) { set(IS_AUTO, auto); return this; } /** * Should this attribute be visible to the user? * * @return <tt>true</tt> if this attribute is visible */ public boolean isVisible() { return requireNonNull(getBoolean(IS_VISIBLE)); } public Attribute setVisible(boolean visible) { set(IS_VISIBLE, visible); return this; } /** * Whether this attribute can be used to aggregate on. Default only attributes of type 'BOOL', 'XREF' and * 'CATEGORICAL' are isAggregatable. * * @return <tt>true</tt> if this attribute is isAggregatable */ public boolean isAggregatable() { return requireNonNull(getBoolean(IS_AGGREGATABLE)); } public Attribute setAggregatable(boolean isAggregatable) { set(IS_AGGREGATABLE, isAggregatable); return this; } /** * For enum fields returns the possible enum values * * @return enum values */ public List<String> getEnumOptions() { String enumOptionsStr = getString(ENUM_OPTIONS); return enumOptionsStr != null ? asList(enumOptionsStr.split(",")) : emptyList(); } public Attribute setEnumOptions(Class<? extends Enum<?>> e) { return setEnumOptions(stream(e.getEnumConstants()).map(Enum::name).collect(toList())); } public Attribute setEnumOptions(List<String> enumOptions) { set(ENUM_OPTIONS, toEnumOptionsString(enumOptions)); return this; } public Long getRangeMin() { return getLong(RANGE_MIN); } public Attribute setRangeMin(Long rangeMin) { set(RANGE_MIN, rangeMin); return this; } public Long getRangeMax() { return getLong(RANGE_MAX); } public Attribute setRangeMax(Long rangeMax) { set(RANGE_MAX, rangeMax); return this; } /** * Whether attribute is readonly * * @return <tt>true</tt> if this attribute is read-only */ public boolean isReadOnly() { return requireNonNull(getBoolean(IS_READ_ONLY)); } public Attribute setReadOnly(boolean readOnly) { set(IS_READ_ONLY, readOnly); return this; } /** * Whether attribute should have an unique value for each entity * * @return <tt>true</tt> if this attribute is unique */ public boolean isUnique() { return requireNonNull(getBoolean(IS_UNIQUE)); } public Attribute setUnique(boolean unique) { set(IS_UNIQUE, unique); return this; } /** * Javascript expression to determine at runtime if the attribute must be visible or not in the form * * @return expression */ public String getVisibleExpression() { return getString(VISIBLE_EXPRESSION); } public Attribute setVisibleExpression(String visibleExpression) { set(VISIBLE_EXPRESSION, visibleExpression); return this; } /** * Javascript expression to validate the value of the attribute */ public String getValidationExpression() { return getString(VALIDATION_EXPRESSION); } public Attribute setValidationExpression(String validationExpression) { set(VALIDATION_EXPRESSION, validationExpression); return this; } public boolean hasDefaultValue() { return getDefaultValue() != null; } /** * Default value expression * * @return attribute default value */ public String getDefaultValue() { return getString(DEFAULT_VALUE); } public Attribute setDefaultValue(String defaultValue) { set(DEFAULT_VALUE, defaultValue); return this; } /** * For int and long fields, the value must be between min and max (included) of the range * * @return attribute value range */ public Range getRange() { Long rangeMin = getRangeMin(); Long rangeMax = getRangeMax(); return rangeMin != null || rangeMax != null ? new Range(rangeMin, rangeMax) : null; } public Attribute setRange(Range range) { set(RANGE_MIN, range.getMin()); set(RANGE_MAX, range.getMax()); return this; } public Attribute getParent() { return getEntity(PARENT, Attribute.class); } public Attribute setParent(Attribute parentAttr) { Attribute currentParent = getParent(); if (currentParent != null) { currentParent.removeChild(this); } set(PARENT, parentAttr); if (parentAttr != null) { parentAttr.addChild(this); } return this; } /** * Get attribute part by name (case insensitive), returns null if not found * * @param attrName attribute name (case insensitive) * @return attribute or null */ public Attribute getChild(String attrName) { Iterable<Attribute> attrParts = getEntities(CHILDREN, Attribute.class); return stream(attrParts.spliterator(), false).filter(attrPart -> attrPart.getName().equals(attrName)) .findFirst().orElse(null); } void addChild(Attribute attrPart) { Iterable<Attribute> attrParts = getEntities(CHILDREN, Attribute.class); set(CHILDREN, concat(attrParts, singletonList(attrPart))); } void removeChild(Attribute attrPart) { Iterable<Attribute> attrParts = getEntities(CHILDREN, Attribute.class); set(CHILDREN, stream(attrParts.spliterator(), false).filter(attr -> !attr.getName().equals(attrPart.getName())) .collect(toList())); } /** * Get all tags for this attribute * * @return attribute tags */ public Iterable<Tag> getTags() { return getEntities(TAGS, Tag.class); } /** * Set tags for this attribute * * @param tags attribute tags * @return this entity */ public Attribute setTags(Iterable<Tag> tags) { set(TAGS, tags); return this; } /** * Add a tag for this attribute * * @param tag attribute tag */ public void addTag(Tag tag) { set(TAGS, concat(getTags(), singletonList(tag))); } /** * Add a tag for this attribute * * @param tag attribute tag */ public void removeTag(Tag tag) { Iterable<Tag> tags = getTags(); removeAll(tags, singletonList(tag)); set(TAGS, tag); } public void setDefaultValues() { setDataType(STRING); setNillable(true); setAuto(false); setVisible(true); setAggregatable(false); setReadOnly(false); setUnique(false); } private static String toEnumOptionsString(List<String> enumOptions) { return !enumOptions.isEmpty() ? enumOptions.stream().collect(joining(",")) : null; } private AttributeType getCachedDataType() { if (cachedDataType == null) { String dataTypeStr = getString(TYPE); cachedDataType = dataTypeStr != null ? AttributeType.toEnum(dataTypeStr) : null; } return cachedDataType; } private void invalidateCachedDataType() { cachedDataType = null; } @Override public String toString() { return "Attribute{" + "name=" + getName() + '}'; } /** * For a reference type attribute, searches the referenced entity for its inversed attribute. * This is the one-to-many attribute that has "mappedBy" set to this attribute. * Returns null if this is not a reference type attribute, or no inverse attribute exists. */ public Attribute getInversedBy() { // FIXME besides checking mappedBy attr name also check attr.getRefEntity().getName if (isReferenceType(this)) { return stream(getRefEntity().getAtomicAttributes().spliterator(), false).filter(Attribute::isMappedBy) .filter(attr -> getName().equals(attr.getMappedBy().getName())).findFirst().orElse(null); } else { return null; } } /** * Determines if this is a reference type attribute whose refEntity has an attribute that has mappedBy set to this * attribute. */ public boolean isInversedBy() { return getInversedBy() != null; } }