From 94b7bad960540ce3164e9052914aee4d58161d4f Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 31 Mar 2026 20:09:02 -0700 Subject: [PATCH 1/4] GitHub Issue 928, 951, 970 & 987 --- .../api/property/DomainPropertyImpl.java | 2795 +++++++++-------- 1 file changed, 1402 insertions(+), 1393 deletions(-) diff --git a/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java b/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java index aa530cf932f..d3c01332986 100644 --- a/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java +++ b/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java @@ -1,1393 +1,1402 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.labkey.experiment.api.property; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Strings; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.Assert; -import org.junit.Test; -import org.labkey.api.data.BooleanFormat; -import org.labkey.api.data.ColumnRenderPropertiesImpl; -import org.labkey.api.data.ConditionalFormat; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.DatabaseIdentifier; -import org.labkey.api.data.JdbcType; -import org.labkey.api.data.PHI; -import org.labkey.api.data.SQLFragment; -import org.labkey.api.data.SqlExecutor; -import org.labkey.api.data.Table; -import org.labkey.api.data.TableInfo; -import org.labkey.api.data.dialect.SqlDialect; -import org.labkey.api.exp.ChangePropertyDescriptorException; -import org.labkey.api.exp.DomainDescriptor; -import org.labkey.api.exp.Lsid; -import org.labkey.api.exp.OntologyManager; -import org.labkey.api.exp.PropertyDescriptor; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.exp.api.StorageProvisioner; -import org.labkey.api.exp.property.Domain; -import org.labkey.api.exp.property.DomainKind; -import org.labkey.api.exp.property.DomainProperty; -import org.labkey.api.exp.property.IPropertyType; -import org.labkey.api.exp.property.IPropertyValidator; -import org.labkey.api.exp.property.Lookup; -import org.labkey.api.exp.property.PropertyService; -import org.labkey.api.exp.property.SystemProperty; -import org.labkey.api.gwt.client.DefaultScaleType; -import org.labkey.api.gwt.client.DefaultValueType; -import org.labkey.api.gwt.client.FacetingBehaviorType; -import org.labkey.api.query.QueryChangeListener; -import org.labkey.api.query.SchemaKey; -import org.labkey.api.security.User; -import org.labkey.api.util.StringExpressionFactory; -import org.labkey.api.util.TestContext; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -public class DomainPropertyImpl implements DomainProperty -{ - private final DomainImpl _domain; - - PropertyDescriptor _pd; - PropertyDescriptor _pdOld; - boolean _deleted; - - private boolean _schemaChanged; - private boolean _schemaImport; - private List _validators; - private List _formats; - private String _defaultValue; - - public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd) - { - this(type, pd, null); - } - - public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd, List formats) - { - _domain = type; - _pd = pd.clone(); - _formats = formats; - } - - @Override - public int getPropertyId() - { - return _pd.getPropertyId(); - } - - @Override - public Container getContainer() - { - return _pd.getContainer(); - } - - @Override - public String getPropertyURI() - { - return _pd.getPropertyURI(); - } - - @Override - public String getName() - { - return _pd.getName(); - } - - @Override - public String getDescription() - { - return _pd.getDescription(); - } - - @Override - public String getFormat() - { - return _pd.getFormat(); - } - - @Override - public String getLabel() - { - return _pd.getLabel(); - } - - @Override - public String getConceptURI() - { - return _pd.getConceptURI(); - } - - @Override - public Domain getDomain() - { - return _domain; - } - - @Override - public IPropertyType getType() - { - return PropertyService.get().getType(getContainer(), _pd.getRangeURI()); - } - - @Override - public boolean isRequired() - { - return _pd.isRequired(); - } - - @Override - public boolean isHidden() - { - return _pd.isHidden(); - } - - @Override - public boolean isDeleted() - { - return _deleted; - } - - @Override - public boolean isShownInInsertView() - { - return _pd.isShownInInsertView(); - } - - @Override - public boolean isShownInDetailsView() - { - return _pd.isShownInDetailsView(); - } - - @Override - public boolean isShownInUpdateView() - { - return _pd.isShownInUpdateView(); - } - - @Override - public boolean isShownInLookupView() - { - return _pd.isShownInLookupView(); - } - - @Override - public boolean isMeasure() - { - return _pd.isMeasure(); - } - - @Override - public boolean isDimension() - { - return _pd.isDimension(); - } - - @Override - public boolean isRecommendedVariable() - { - return _pd.isRecommendedVariable(); - } - - @Override - public DefaultScaleType getDefaultScale() - { - return _pd.getDefaultScale(); - } - - @Override - public PHI getPHI() - { - return _pd.getPHI(); - } - - @Override - public String getRedactedText() { return _pd.getRedactedText(); } - - @Override - public boolean isExcludeFromShifting() - { - return _pd.isExcludeFromShifting(); - } - - @Override - public boolean isMvEnabled() - { - return _pd.isMvEnabled(); - } - - @Override - public boolean isMvEnabledForDrop() - { - if (null != _pdOld) - return _pdOld.isMvEnabled(); // if we need to drop/recreate we care about the old one - return _pd.isMvEnabled(); - } - - @Override - public void delete() - { - _deleted = true; - } - - @Override - public void setSchemaImport(boolean isSchemaImport) - { - // if this flag is set True then the column is dropped and recreated by its Domain if there is a type change - _schemaImport = isSchemaImport; - } - - @Override - public void setName(String name) - { - if (Strings.CS.equals(name, getName())) - return; - edit().setName(name); - } - - @Override - public void setDescription(String description) - { - if (Strings.CS.equals(description, getDescription())) - return; - edit().setDescription(description); - } - - @Override - public void setType(IPropertyType domain) - { - edit().setRangeURI(domain.getTypeURI()); - } - - @Override - public void setPropertyURI(String uri) - { - if (Strings.CS.equals(uri, getPropertyURI())) - return; - edit().setPropertyURI(uri); - } - - @Override - public void setRangeURI(String rangeURI) - { - if (Strings.CS.equals(rangeURI, getRangeURI())) - return; - editSchema().setRangeURI(rangeURI); - } - - @Override - public String getRangeURI() - { - return _pd.getRangeURI(); - } - - @Override - public void setFormat(String s) - { - if (Strings.CS.equals(s, getFormat())) - return; - edit().setFormat(s); - } - - @Override - public void setLabel(String caption) - { - if (Strings.CS.equals(caption, getLabel())) - return; - edit().setLabel(caption); - } - - @Override - public void setConceptURI(String conceptURI) - { - if (Strings.CS.equals(conceptURI, getConceptURI())) - return; - edit().setConceptURI(conceptURI); - } - - @Override - public void setRequired(boolean required) - { - if (required == isRequired()) - return; - edit().setRequired(required); - } - - @Override - public void setHidden(boolean hidden) - { - if (hidden == isHidden()) - return; - edit().setHidden(hidden); - } - - @Override - public void setShownInDetailsView(boolean shown) - { - if (shown == isShownInDetailsView()) - return; - edit().setShownInDetailsView(shown); - } - - @Override - public void setShownInInsertView(boolean shown) - { - if (shown == isShownInInsertView()) - return; - edit().setShownInInsertView(shown); - } - - @Override - public void setShownInUpdateView(boolean shown) - { - if (shown == isShownInUpdateView()) - return; - edit().setShownInUpdateView(shown); - } - - @Override - public void setShownInLookupView(boolean shown) - { - if (shown == isShownInLookupView()) - return; - edit().setShownInLookupView(shown); - } - - @Override - public void setMeasure(boolean isMeasure) - { - // UNDONE: isMeasure() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isMeasure == isMeasure()) - return; - edit().setMeasure(isMeasure); - } - - @Override - public void setDimension(boolean isDimension) - { - // UNDONE: isDimension() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isDimension == isDimension()) - return; - edit().setDimension(isDimension); - } - - @Override - public void setRecommendedVariable(boolean isRecommendedVariable) - { - if (!isEdited() && isRecommendedVariable == isRecommendedVariable()) - return; - edit().setRecommendedVariable(isRecommendedVariable); - } - - @Override - public void setDefaultScale(DefaultScaleType defaultScale) - { - if (!isEdited() && getDefaultScale() == defaultScale) - return; - - edit().setDefaultScale(defaultScale); - } - - @Override - public void setPhi(PHI phi) - { - if (!isEdited() && getPHI() == phi) - return; - edit().setPHI(phi); - } - - @Override - public void setRedactedText(String redactedText) - { - if (!isEdited() && ((getRedactedText() != null && getRedactedText().equals(redactedText)) - || (getRedactedText() == null && redactedText == null))) - return; - edit().setRedactedText(redactedText); - } - - @Override - public void setExcludeFromShifting(boolean isExcludeFromShifting) - { - // UNDONE: isExcludeFromShifting() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isExcludeFromShifting == isExcludeFromShifting()) - return; - edit().setExcludeFromShifting(isExcludeFromShifting); - } - - @Override - public void setMvEnabled(boolean mv) - { - if (mv == isMvEnabled()) - return; - edit().setMvEnabled(mv); - } - - @Override - public void setScale(int scale) - { - if (scale == getScale()) - return; - edit().setScale(scale); - } - - /** Need the string version of this method because it's called by reflection and must match by name */ - public void setImportAliases(String aliases) - { - if (Strings.CS.equals(aliases, getImportAliases())) - return; - edit().setImportAliases(aliases); - } - - /** Need the string version of this method because it's called by reflection and must match by name */ - public String getImportAliases() - { - return _pd.getImportAliases(); - } - - @Override - public void setImportAliasSet(Set aliases) - { - String current = getImportAliases(); - String newAliases = ColumnRenderPropertiesImpl.convertToString(aliases); - if (Strings.CS.equals(current, newAliases)) - return; - edit().setImportAliasesSet(aliases); - } - - @Override - public Set getImportAliasSet() - { - return _pd.getImportAliasSet(); - } - - @Override - public void setURL(String url) - { - if (Strings.CS.equals(getURL(), url)) - return; - - if (null == url) - edit().setURL(null); - else - edit().setURL(StringExpressionFactory.createURL(url)); - } - - @Override - public String getURL() - { - return _pd.getURL() == null ? null : _pd.getURL().toString(); - } - - @Override - public void setURLTarget(String urlTarget) - { - if (Strings.CS.equals(getURLTarget(), urlTarget)) - return; - edit().setURLTarget(urlTarget); - } - - @Override - public String getURLTarget() - { - return _pd.getURLTarget(); - } - - private boolean isEdited() - { - return null != _pdOld; - } - - private PropertyDescriptor editSchema() - { - PropertyDescriptor pd = edit(); - _schemaChanged = true; - _pd.clearPropertyType(); - return pd; - } - - public boolean isRecreateRequired() - { - return _schemaChanged && _schemaImport; - } - - public void markAsNew() - { - assert isRecreateRequired() && !isNew(); - _pd.setPropertyId(0); - } - - private PropertyDescriptor edit() - { - if (_pdOld == null) - { - _pdOld = _pd; - _pd = _pdOld.clone(); - } - return _pd; - } - - @Override - public PropertyType getPropertyType() - { - return _pd.getPropertyType(); - } - - @Override - public JdbcType getJdbcType() - { - return _pd.getPropertyType().getJdbcType(); - } - - @Override - public int getScale() - { - return _pd.getScale(); - } - - @Override - public String getInputType() - { - return _pd.getPropertyType().getInputType(); - } - - @Override - public DefaultValueType getDefaultValueTypeEnum() - { - return _pd.getDefaultValueTypeEnum(); - } - - @Override - public void setDefaultValueTypeEnum(DefaultValueType defaultValueType) - { - _pd.setDefaultValueTypeEnum(defaultValueType); - } - - public String getDefaultValueType() - { - return _pd.getDefaultValueType(); - } - - @Override - public void setDefaultValueType(String defaultValueTypeName) - { - if (getDefaultValueType() != null && getDefaultValueType().equals(defaultValueTypeName)) - return; - - if (getDefaultValueType() == null && defaultValueTypeName == null) - return; // if both are null, don't call edit(), with marks property as dirty - - edit().setDefaultValueType(defaultValueTypeName); - } - - @Override - public void setDefaultValue(String value) - { - _defaultValue = value; - } - - public String getDefaultValue() - { - return _defaultValue; - } - - @Override - public Lookup getLookup() - { - return _pd.getLookup(); - } - - @Override - public void setLookup(Lookup lookup) - { - Lookup current = getLookup(); - - if (current == lookup) - return; - - // current will return null if the schema or query is null so check - // for this case in the passed in lookup - if (current == null) - if (lookup.getQueryName() == null || lookup.getSchemaKey() == null) - return; - - if (current != null && current.equals(lookup)) - return; - - if (lookup == null) - { - edit().setLookupContainer(null); - edit().setLookupSchema(null); - edit().setLookupQuery(null); - return; - } - if (lookup.getContainer() == null) - { - edit().setLookupContainer(null); - } - else - { - edit().setLookupContainer(lookup.getContainer().getId()); - } - edit().setLookupQuery(lookup.getQueryName()); - edit().setLookupSchema(Objects.toString(lookup.getSchemaKey(),null)); - } - - @Override - public void setScannable(boolean scannable) - { - if (scannable != isScannable()) - edit().setScannable(scannable); - } - - @Override - public void setOldPropertyDescriptor(PropertyDescriptor oldPropertyDescriptor) - { - if (isEdited()) - return; - - _pdOld = oldPropertyDescriptor.clone(); - } - - @Override - public boolean isScannable() - { - return _pd.isScannable(); - } - - @Override - public void setPrincipalConceptCode(String code) - { - if (!Strings.CS.equals(code, getPrincipalConceptCode())) - edit().setPrincipalConceptCode(code); - } - - @Override - public String getPrincipalConceptCode() - { - return _pd.getPrincipalConceptCode(); - } - - @Override - public String getSourceOntology() - { - return _pd.getSourceOntology(); - } - - @Override - public void setSourceOntology(String sourceOntology) - { - if (!Strings.CS.equals(sourceOntology, getSourceOntology())) - edit().setSourceOntology(sourceOntology); - } - - @Override - public String getConceptSubtree() - { - return _pd.getConceptSubtree(); - } - - @Override - public void setConceptSubtree(String path) - { - if (!Strings.CS.equals(path, getConceptSubtree())) - edit().setConceptSubtree(path); - } - - @Override - public String getConceptImportColumn() - { - return _pd.getConceptImportColumn(); - } - - @Override - public void setConceptImportColumn(String conceptImportColumn) - { - if (!Strings.CS.equals(conceptImportColumn, getConceptImportColumn())) - edit().setConceptImportColumn(conceptImportColumn); - } - - @Override - public String getConceptLabelColumn() - { - return _pd.getConceptLabelColumn(); - } - - @Override - public void setConceptLabelColumn(String conceptLabelColumn) - { - if (!Strings.CS.equals(conceptLabelColumn, getConceptLabelColumn())) - edit().setConceptLabelColumn(conceptLabelColumn); - } - - @Override - public void setDerivationDataScope(String scope) - { - if (!Strings.CS.equals(scope, getDerivationDataScope())) - edit().setDerivationDataScope(scope); - } - - @Override - public String getDerivationDataScope() - { - return _pd.getDerivationDataScope(); - } - - @Override - public PropertyDescriptor getPropertyDescriptor() - { - return _pd; - } - - @Override - public List getConditionalFormats() - { - return ensureConditionalFormats(); - } - - public boolean isNew() - { - return _pd.getPropertyId() == 0; - } - - // Scenario to swap property descriptors on study upload to or from a system property, instead of updating the - // current property descriptor. Avoids overwriting a system property. - public boolean isSystemPropertySwap() - { - if (_pd.getPropertyId() == 0 && _pd.getPropertyURI() != null && _pdOld != null && _pdOld.getPropertyURI() != null - && !_pd.getPropertyURI().equals(_pdOld.getPropertyURI())) - { - return SystemProperty.getProperties().stream().anyMatch(sp -> - sp.getPropertyURI().equals(_pd.getPropertyURI()) || sp.getPropertyURI().equals(_pdOld.getPropertyURI())); - } - - return false; - } - - public boolean isDirty() - { - if (_pdOld != null) return true; - - for (PropertyValidatorImpl v : ensureValidators()) - { - if (v.isDirty() || v.isNew()) - return true; - } - return false; - } - - public void delete(User user) - { - DomainPropertyManager.get().removeValidatorsForPropertyDescriptor(getContainer(), getPropertyId()); - DomainPropertyManager.get().deleteConditionalFormats(getPropertyId()); - - DomainKind kind = getDomain().getDomainKind(); - if (null != kind) - kind.deletePropertyDescriptor(getDomain(), user, _pd); - OntologyManager.removePropertyDescriptorFromDomain(this); - } - - public void save(User user, DomainDescriptor dd, int sortOrder) throws ChangePropertyDescriptorException - { - if (isSystemPropertySwap()) - { - _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); - OntologyManager.removePropertyDescriptorFromDomain(new DomainPropertyImpl((DomainImpl) getDomain(), _pdOld)); - } - else if (isNew()) - { - _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); - } - else if (_pdOld != null) - { - PropertyType oldType = _pdOld.getPropertyType(); - PropertyType newType = _pd.getPropertyType(); - boolean changedType = false; - if (oldType.getJdbcType() != newType.getJdbcType()) - { - if (newType.getJdbcType().isText() || - (oldType.getJdbcType().isInteger() && newType.getJdbcType().isNumeric())) - { - changedType = true; - if (newType.getJdbcType().isText()) - { - // Remove any previously set formatting string as it won't apply to a text field - _pd.setFormat(null); - } - } - else if (newType.getJdbcType().isDateOrTime() && oldType.getJdbcType().isDateOrTime()) - { - changedType = true; - _pd.setFormat(null); - } - else if (newType == PropertyType.MULTI_CHOICE || oldType == PropertyType.MULTI_CHOICE) - { - changedType = true; - _pd.setFormat(null); - } - else - { - throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.getJdbcType() + " to " + newType.getJdbcType() + "."); - } - } - - // Issue 44711: Prevent attachment and file field types from being converted to a different type - if (PropertyType.FILE_LINK.getInputType().equalsIgnoreCase(oldType.getInputType()) && oldType != newType) - throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.name() + " to " + newType.name() + "."); - - OntologyManager.validatePropertyDescriptor(_pd); - Table.update(user, OntologyManager.getTinfoPropertyDescriptor(), _pd, _pdOld.getPropertyId()); - OntologyManager.ensurePropertyDomain(_pd, dd, sortOrder); - - boolean hasProvisioner = null != getDomain().getDomainKind() && null != getDomain().getDomainKind().getStorageSchemaName() && dd.getStorageTableName() != null; - SqlDialect dialect = OntologyManager.getExpSchema().getSqlDialect(); - - if (hasProvisioner) - { - boolean mvAdded = !_pdOld.isMvEnabled() && _pd.isMvEnabled(); - boolean mvDropped = _pdOld.isMvEnabled() && !_pd.isMvEnabled(); - boolean propRenamed = !_pdOld.getName().equals(_pd.getName()); - boolean propResized = _pd.isStringType() && _pdOld.getScale() != _pd.getScale(); - - // Drop first, so rename doesn't have to worry about it - if (mvDropped) - ((StorageProvisionerImpl)StorageProvisioner.get()).dropMvIndicator(this, _pdOld); - - if (propRenamed) - StorageProvisionerImpl.get().renameProperty(this.getDomain(), this, _pdOld, mvDropped); - - if (changedType) - { - var domainKind = _domain.getDomainKind(); - if (domainKind == null) - throw new ChangePropertyDescriptorException("Cannot change property type for domain, unknown domain kind."); - - StorageProvisionerImpl.get().changePropertyType(this.getDomain(), this); - if (_pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) - { - updateBooleanValue( - new SQLFragment().appendIdentifier(domainKind.getStorageSchemaName()).append(".").appendIdentifier(_domain.getStorageTableName()), - _pd.getLegalSelectName(dialect), _pdOld.getFormat(), null); // GitHub Issue #647 - } - - TableInfo table = domainKind.getTableInfo(user, getContainer(), _domain, ContainerFilter.getUnsafeEverythingFilter()); - if (table != null && _pdOld.getPropertyType() != null && table.getSchema().getSqlDialect().isPostgreSQL()) - QueryChangeListener.QueryPropertyChange.handleColumnTypeChange(_pdOld, _pd, SchemaKey.fromString(table.getUserSchema().getSchemaName()), table.getName(), user, getContainer()); - } - else if (propResized) - StorageProvisionerImpl.get().resizeProperty(this.getDomain(), this, _pdOld.getScale()); - - if (mvAdded) - StorageProvisionerImpl.get().addMvIndicator(this); - } - else if (changedType) - { - if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isText()) - { - new SqlExecutor(OntologyManager.getExpSchema()).execute( - new SQLFragment("UPDATE "). - append(OntologyManager.getTinfoObjectProperty()). - append(" SET StringValue = DateTimeValue, DateTimeValue = NULL WHERE PropertyId = ?"). - add(_pdOld.getPropertyId())); - } - else if (!oldType.getJdbcType().isText() && newType.getJdbcType().isText()) - { - new SqlExecutor(OntologyManager.getExpSchema()).execute( - new SQLFragment("UPDATE "). - append(OntologyManager.getTinfoObjectProperty()). - append(" SET StringValue = FloatValue, FloatValue = NULL WHERE PropertyId = ?"). - add(_pdOld.getPropertyId())); - } - else if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isDateOrTime()) - { - String sqlTypeName = dialect.getSqlTypeName(newType.getJdbcType()); - String update = String.format("CAST(DateTimeValue AS %s)", sqlTypeName); - if (newType.getJdbcType() == JdbcType.TIME) - update = dialect.getDateTimeToTimeCast("DateTimeValue"); - SQLFragment sqlFragment = new SQLFragment("UPDATE ") - .append(OntologyManager.getTinfoObjectProperty()) - .append(" SET DateTimeValue = ") - .append(update) - .append(" WHERE PropertyId = ?") - .add(_pdOld.getPropertyId()); - new SqlExecutor(OntologyManager.getExpSchema()).execute(sqlFragment); - } - else //noinspection StatementWithEmptyBody - if (oldType.getJdbcType().isInteger() && newType.getJdbcType().isReal()) - { - // Since exp.ObjectProperty stores these types in the same column, there's nothing for us to do - } - else - { - throw new ChangePropertyDescriptorException("Cannot convert from " + oldType.getJdbcType() + " to " + newType.getJdbcType() + " for non-provisioned table"); - } - } - - if (changedType && _pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) - { - updateBooleanValue(OntologyManager.getTinfoObjectProperty().getSQLName(), dialect.makeDatabaseIdentifier("StringValue"), _pdOld.getFormat(), new SQLFragment("PropertyId = ?", _pdOld.getPropertyId())); - } - } - else - { - OntologyManager.ensurePropertyDomain(_pd, _domain._dd, sortOrder); - } - - _pdOld = null; - _schemaChanged = false; - _schemaImport = false; - - for (PropertyValidatorImpl validator : ensureValidators()) - { - if (validator.isDeleted()) - DomainPropertyManager.get().removePropertyValidator(this, validator); - else - DomainPropertyManager.get().savePropertyValidator(user, this, validator); - } - - DomainPropertyManager.get().saveConditionalFormats(user, getPropertyDescriptor(), ensureConditionalFormats()); - } - - /** - * Format values in columns that were just converted from booleans to strings with the DB's default type conversion. - * Postgres will now have 'true' and 'false', and SQLServer will have '0' and '1'. Use the format string to use the - * preferred format, and standardize on 'true' and 'false' in the absence of an explicitly configured format. - */ - private void updateBooleanValue(SQLFragment schemaTable, DatabaseIdentifier column, String formatString, @Nullable SQLFragment whereClause) - { - BooleanFormat f = BooleanFormat.getInstance(formatString); - String trueValue = StringUtils.trimToNull(f.format(true)); - String falseValue = StringUtils.trimToNull(f.format(false)); - String nullValue = StringUtils.trimToNull(f.format(null)); - SQLFragment sql = new SQLFragment("UPDATE ").append(schemaTable).append(" SET "). - appendIdentifier(column).append(" = CASE WHEN "). - appendIdentifier(column).append(" IN ('1', 'true') THEN ? WHEN "). - appendIdentifier(column).append(" IN ('0', 'false') THEN ? ELSE ? END"); - sql.add(trueValue); - sql.add(falseValue); - sql.add(nullValue); - if (whereClause != null) - { - sql.append(" WHERE "); - sql.append(whereClause); - } - new SqlExecutor(OntologyManager.getExpSchema()).execute(sql); - } - - @Override - @NotNull - public List getValidators() - { - return Collections.unmodifiableList(ensureValidators()); - } - - @Override - public void addValidator(IPropertyValidator validator) - { - if (validator != null) - { - if (0 != validator.getPropertyId() && getPropertyId() != validator.getPropertyId()) - throw new IllegalStateException(); - - // Ensure validator is a valid kind (ex. urn:lsid:labkey.com:PropertyValidator:length is no longer valid) - if ( null != PropertyService.get().getValidatorKind(validator.getTypeURI()) ) - { - PropertyValidator impl = new PropertyValidator(); - impl.copy(validator); - impl.setPropertyId(getPropertyId()); - ensureValidators().add(new PropertyValidatorImpl(impl)); - } - } - } - - @Override - public void removeValidator(IPropertyValidator validator) - { - int idx = ensureValidators().indexOf(validator); - if (idx != -1) - { - PropertyValidatorImpl impl = ensureValidators().get(idx); - impl.delete(); - } - } - - @Override - public void removeValidator(long validatorId) - { - if (validatorId == 0) return; - - for (PropertyValidatorImpl imp : ensureValidators()) - { - if (imp.getRowId() == validatorId) - { - imp.delete(); - break; - } - } - } - - @Override - public void copyFrom(DomainProperty propSrc, Container targetContainer) - { - setDescription(propSrc.getDescription()); - setFormat(propSrc.getFormat()); - setLabel(propSrc.getLabel()); - setName(propSrc.getName()); - setDescription(propSrc.getDescription()); - setConceptURI(propSrc.getConceptURI()); - setType(propSrc.getType()); - setDimension(propSrc.isDimension()); - setMeasure(propSrc.isMeasure()); - setRecommendedVariable(propSrc.isRecommendedVariable()); - setDefaultScale(propSrc.getDefaultScale()); - setRequired(propSrc.isRequired()); - setExcludeFromShifting(propSrc.isExcludeFromShifting()); - setFacetingBehavior(propSrc.getFacetingBehavior()); - setImportAliasSet(propSrc.getImportAliasSet()); - setPhi(propSrc.getPHI()); - setURL(propSrc.getURL()); - setURLTarget(propSrc.getURLTarget()); - setHidden(propSrc.isHidden()); - setShownInDetailsView(propSrc.isShownInDetailsView()); - setShownInInsertView(propSrc.isShownInInsertView()); - setShownInUpdateView(propSrc.isShownInUpdateView()); - setShownInLookupView(propSrc.isShownInLookupView()); - setMvEnabled(propSrc.isMvEnabled()); - setDefaultValueTypeEnum(propSrc.getDefaultValueTypeEnum()); - setScale(propSrc.getScale()); - setScannable(propSrc.isScannable()); - - setPrincipalConceptCode(propSrc.getPrincipalConceptCode()); - setSourceOntology(propSrc.getSourceOntology()); - setConceptSubtree(propSrc.getConceptSubtree()); - setConceptImportColumn(propSrc.getConceptImportColumn()); - setConceptLabelColumn(propSrc.getConceptLabelColumn()); - setDerivationDataScope(propSrc.getDerivationDataScope()); - - // check to see if we're moving a lookup column to another container: - Lookup lookup = propSrc.getLookup(); - if (lookup != null && !getContainer().equals(targetContainer)) - { - // we need to update the lookup properties if the lookup container is either the source or the destination container - if (lookup.getContainer() == null) - lookup.setContainer(propSrc.getContainer()); - else if (lookup.getContainer().equals(targetContainer)) - lookup.setContainer(null); - } - setLookup(lookup); - } - - @Override - public void setConditionalFormats(List formats) - { - String newVal = ConditionalFormat.toStringVal(formats); - String oldVal = ConditionalFormat.toStringVal(getConditionalFormats()); - - if (!Objects.equals(newVal, oldVal)) - edit(); - - _formats = formats; - } - - private List ensureValidators() - { - if (_validators == null) - { - _validators = new ArrayList<>(); - for (PropertyValidator validator : DomainPropertyManager.get().getValidators(this)) - { - _validators.add(new PropertyValidatorImpl(validator)); - } - } - return _validators; - } - - private List ensureConditionalFormats() - { - if (_formats == null) - { - _formats = new ArrayList<>(); - _formats.addAll(DomainPropertyManager.get().getConditionalFormats(this)); - } - return _formats; - } - - public PropertyDescriptor getOldProperty() - { - return _pdOld; - } - - @Override - public FacetingBehaviorType getFacetingBehavior() - { - return _pd.getFacetingBehaviorType(); - } - - @Override - public void setFacetingBehavior(FacetingBehaviorType type) - { - if (getFacetingBehavior() == type) - return; - - edit().setFacetingBehaviorType(type); - } - - @Override - public int hashCode() - { - return _pd.hashCode(); - } - - @Override - public boolean equals(Object obj) - { - if (obj == this) - return true; - if (!(obj instanceof DomainPropertyImpl)) - return false; - // once a domain property has been edited, it no longer equals any other domain property: - if (_pdOld != null || ((DomainPropertyImpl) obj)._pdOld != null) - return false; - return (_pd.equals(((DomainPropertyImpl) obj)._pd)); - } - - @Override - public String toString() - { - return super.toString() + _pd.getPropertyURI(); - } - - public Map getAuditRecordMap(@Nullable String validatorStr, @Nullable String conditionalFormatStr) - { - Map map = new LinkedHashMap<>(); - if (!StringUtils.isEmpty(getName())) - map.put("Name", getName()); - if (!StringUtils.isEmpty(getLabel())) - map.put("Label", getLabel()); - if (null != getPropertyType()) - map.put("Type", getPropertyType().getXarName()); - if (getPropertyType().getJdbcType().isText()) - map.put("Scale", getScale()); - if (!StringUtils.isEmpty(getDescription())) - map.put("Description", getDescription()); - if (!StringUtils.isEmpty(getFormat())) - map.put("Format", getFormat()); - if (!StringUtils.isEmpty(getURL())) - map.put("URL", getURL()); - if (!StringUtils.isEmpty(getURLTarget())) - map.put("URLTarget", getURLTarget()); - if (getPHI() != null) - map.put("PHI", getPHI().getLabel()); - if (getDefaultScale() != null) - map.put("DefaultScale", getDefaultScale().getLabel()); - map.put("Required", isRequired()); - map.put("Hidden", isHidden()); - map.put("MvEnabled", isMvEnabled()); - map.put("Measure", isMeasure()); - map.put("Dimension", isDimension()); - map.put("ShownInInsert", isShownInInsertView()); - map.put("ShownInDetails", isShownInDetailsView()); - map.put("ShownInUpdate", isShownInUpdateView()); - map.put("ShownInLookupView", isShownInLookupView()); - map.put("RecommendedVariable", isRecommendedVariable()); - map.put("ExcludedFromShifting", isExcludeFromShifting()); - map.put("Scannable", isScannable()); - if (!StringUtils.isEmpty(getDerivationDataScope())) - map.put("DerivationDataScope", getDerivationDataScope()); - String importAliasStr = StringUtils.join(getImportAliasSet(), ","); - if (!StringUtils.isEmpty(importAliasStr)) - map.put("ImportAliases", importAliasStr); - if (getDefaultValueTypeEnum() != null) - map.put("DefaultValueType", getDefaultValueTypeEnum().getLabel()); - if (getLookup() != null) - map.put("Lookup", getLookup().toJSONString()); - - if (!StringUtils.isEmpty(validatorStr)) - map.put("Validator", validatorStr); - if (!StringUtils.isEmpty(conditionalFormatStr)) - map.put("ConditionalFormat", conditionalFormatStr); - - return map; - } - - public static class TestCase extends Assert - { - private PropertyDescriptor _pd; - private DomainPropertyImpl _dp; - - @Test - public void testUpdateDomainPropertyFromDescriptor() - { - Container c = ContainerManager.ensureContainer("/_DomainPropertyImplTest", TestContext.get().getUser()); - String domainURI = new Lsid("Junit", "DD", "Domain1").toString(); - Domain d = PropertyService.get().createDomain(c, domainURI, "Domain1"); - - resetProperties(d, domainURI, c); - - // verify no change - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertFalse(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - - // change a property - _pd.setPHI(PHI.Restricted); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - assertTrue(_dp.getPHI() == _pd.getPHI()); - - // Issue #18738 change the schema outside of a schema reload and verify that the column - // change the schema but don't mark the property as "Schema Import" - // this will allow whatever type changes the UI allows (text -> multiline, for example) - resetProperties(d, domainURI, c); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertTrue(_dp._schemaChanged); - assertFalse(_dp.isRecreateRequired()); - assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); - - // setting schema import to true will enable the _schemaChanged flag to toggle - // so it should be set true here - resetProperties(d, domainURI, c); - _dp.setSchemaImport(true); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertTrue(_dp._schemaChanged); - assertTrue(_dp.isRecreateRequired()); - assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); - - // verify no change when setting value to the same value as it was - resetProperties(d, domainURI, c); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); - _pd.setPHI(PHI.NotPHI); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertFalse(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - assertFalse(_dp.isRecreateRequired()); - - // verify Lookup is set to null with null schema - resetProperties(d, domainURI, c); - verifyLookup(null, "lkSchema", null, true); - - // verify Lookup is set to null with null query - resetProperties(d, domainURI, c); - verifyLookup(null, null, "lkQuery",true); - - // verify Lookup is set to null with invalid container - resetProperties(d, domainURI, c); - verifyLookup("bogus", null, "lkQuery",true); - - // verify Lookup is set with valid schema and query - resetProperties(d, domainURI, c); - verifyLookup(null, "lkSchema", "lkQuery",true); - - // verify Lookup is set with valid container, schema and query - resetProperties(d, domainURI, c); - verifyLookup(c.getId(), "lkSchema1", "lkQuery2",true); - - // no cleanup as we never persisted anything - } - - private void verifyLookup(String containerId, String schema, String query, Boolean expectedDirty) - { - _pd.setLookupContainer(containerId); - _pd.setLookupQuery(query); - _pd.setLookupSchema(schema); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty() == expectedDirty); - assertFalse(_dp._schemaChanged); - - // verify the lookup object returned - Lookup l = _dp.getLookup(); - - if (l == null) - { - // lookup can be null if we specified a containerId that is invalid or - // we specified a valid containerId (including null) but schema or query is null - if (containerId != null && null == ContainerManager.getForId(containerId)) - assertTrue(true); - else if (query == null || schema == null) - assertTrue(true); - else - assertTrue(false); - } - else - { - if (containerId != null) - assertTrue(Strings.CS.equals(l.getContainer().getId(), _pd.getLookupContainer())); - - assertTrue(Strings.CS.equals(l.getQueryName(), _pd.getLookupQuery())); - assertTrue(Strings.CS.equals(l.getSchemaKey().toString(), _pd.getLookupSchema())); - } - } - - private void resetProperties(Domain d, String domainUri, Container c) - { - _pd = getPropertyDescriptor(c, domainUri); - _dp = (DomainPropertyImpl) d.addProperty(); - _pd.copyTo(_dp.getPropertyDescriptor()); - } - - - private PropertyDescriptor getPropertyDescriptor(Container c, String domainURI) - { - PropertyDescriptor pd = new PropertyDescriptor(); - pd.setPropertyURI(domainURI + ":column"); - pd.setName("column"); - pd.setLabel("label"); - pd.setConceptURI(null); - pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); - pd.setContainer(c); - pd.setDescription("description"); - pd.setURL(StringExpressionFactory.createURL((String)null)); - pd.setURLTarget(null); - pd.setImportAliases(null); - pd.setRequired(false); - pd.setHidden(false); - pd.setShownInInsertView(true); - pd.setShownInUpdateView(true); - pd.setShownInDetailsView(true); - pd.setDimension(false); - pd.setMeasure(true); - pd.setRecommendedVariable(false); - pd.setDefaultScale(DefaultScaleType.LINEAR); - pd.setFormat(null); - pd.setMvEnabled(false); - pd.setLookupContainer(c.getId()); - pd.setLookupSchema("lkSchema"); - pd.setLookupQuery("lkQuery"); - pd.setFacetingBehaviorType(FacetingBehaviorType.AUTOMATIC); - pd.setPHI(PHI.NotPHI); - pd.setExcludeFromShifting(false); - return pd; - } - } - - -} +/* + * Copyright (c) 2008-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.labkey.experiment.api.property; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.labkey.api.data.BooleanFormat; +import org.labkey.api.data.ColumnRenderPropertiesImpl; +import org.labkey.api.data.ConditionalFormat; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DatabaseIdentifier; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.PHI; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.dialect.SqlDialect; +import org.labkey.api.exp.ChangePropertyDescriptorException; +import org.labkey.api.exp.DomainDescriptor; +import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.exp.api.StorageProvisioner; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainKind; +import org.labkey.api.exp.property.DomainProperty; +import org.labkey.api.exp.property.IPropertyType; +import org.labkey.api.exp.property.IPropertyValidator; +import org.labkey.api.exp.property.Lookup; +import org.labkey.api.exp.property.PropertyService; +import org.labkey.api.exp.property.SystemProperty; +import org.labkey.api.gwt.client.DefaultScaleType; +import org.labkey.api.gwt.client.DefaultValueType; +import org.labkey.api.gwt.client.FacetingBehaviorType; +import org.labkey.api.query.QueryChangeListener; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.security.User; +import org.labkey.api.util.StringExpressionFactory; +import org.labkey.api.util.TestContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static org.labkey.api.data.ColumnRenderPropertiesImpl.TEXT_CHOICE_CONCEPT_URI; + +public class DomainPropertyImpl implements DomainProperty +{ + private final DomainImpl _domain; + + PropertyDescriptor _pd; + PropertyDescriptor _pdOld; + boolean _deleted; + + private boolean _schemaChanged; + private boolean _schemaImport; + private List _validators; + private List _formats; + private String _defaultValue; + + public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd) + { + this(type, pd, null); + } + + public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd, List formats) + { + _domain = type; + _pd = pd.clone(); + _formats = formats; + } + + @Override + public int getPropertyId() + { + return _pd.getPropertyId(); + } + + @Override + public Container getContainer() + { + return _pd.getContainer(); + } + + @Override + public String getPropertyURI() + { + return _pd.getPropertyURI(); + } + + @Override + public String getName() + { + return _pd.getName(); + } + + @Override + public String getDescription() + { + return _pd.getDescription(); + } + + @Override + public String getFormat() + { + return _pd.getFormat(); + } + + @Override + public String getLabel() + { + return _pd.getLabel(); + } + + @Override + public String getConceptURI() + { + return _pd.getConceptURI(); + } + + @Override + public Domain getDomain() + { + return _domain; + } + + @Override + public IPropertyType getType() + { + return PropertyService.get().getType(getContainer(), _pd.getRangeURI()); + } + + @Override + public boolean isRequired() + { + return _pd.isRequired(); + } + + @Override + public boolean isHidden() + { + return _pd.isHidden(); + } + + @Override + public boolean isDeleted() + { + return _deleted; + } + + @Override + public boolean isShownInInsertView() + { + return _pd.isShownInInsertView(); + } + + @Override + public boolean isShownInDetailsView() + { + return _pd.isShownInDetailsView(); + } + + @Override + public boolean isShownInUpdateView() + { + return _pd.isShownInUpdateView(); + } + + @Override + public boolean isShownInLookupView() + { + return _pd.isShownInLookupView(); + } + + @Override + public boolean isMeasure() + { + return _pd.isMeasure(); + } + + @Override + public boolean isDimension() + { + return _pd.isDimension(); + } + + @Override + public boolean isRecommendedVariable() + { + return _pd.isRecommendedVariable(); + } + + @Override + public DefaultScaleType getDefaultScale() + { + return _pd.getDefaultScale(); + } + + @Override + public PHI getPHI() + { + return _pd.getPHI(); + } + + @Override + public String getRedactedText() { return _pd.getRedactedText(); } + + @Override + public boolean isExcludeFromShifting() + { + return _pd.isExcludeFromShifting(); + } + + @Override + public boolean isMvEnabled() + { + return _pd.isMvEnabled(); + } + + @Override + public boolean isMvEnabledForDrop() + { + if (null != _pdOld) + return _pdOld.isMvEnabled(); // if we need to drop/recreate we care about the old one + return _pd.isMvEnabled(); + } + + @Override + public void delete() + { + _deleted = true; + } + + @Override + public void setSchemaImport(boolean isSchemaImport) + { + // if this flag is set True then the column is dropped and recreated by its Domain if there is a type change + _schemaImport = isSchemaImport; + } + + @Override + public void setName(String name) + { + if (Strings.CS.equals(name, getName())) + return; + edit().setName(name); + } + + @Override + public void setDescription(String description) + { + if (Strings.CS.equals(description, getDescription())) + return; + edit().setDescription(description); + } + + @Override + public void setType(IPropertyType domain) + { + edit().setRangeURI(domain.getTypeURI()); + } + + @Override + public void setPropertyURI(String uri) + { + if (Strings.CS.equals(uri, getPropertyURI())) + return; + edit().setPropertyURI(uri); + } + + @Override + public void setRangeURI(String rangeURI) + { + if (Strings.CS.equals(rangeURI, getRangeURI())) + return; + editSchema().setRangeURI(rangeURI); + } + + @Override + public String getRangeURI() + { + return _pd.getRangeURI(); + } + + @Override + public void setFormat(String s) + { + if (Strings.CS.equals(s, getFormat())) + return; + edit().setFormat(s); + } + + @Override + public void setLabel(String caption) + { + if (Strings.CS.equals(caption, getLabel())) + return; + edit().setLabel(caption); + } + + @Override + public void setConceptURI(String conceptURI) + { + if (Strings.CS.equals(conceptURI, getConceptURI())) + return; + edit().setConceptURI(conceptURI); + } + + @Override + public void setRequired(boolean required) + { + if (required == isRequired()) + return; + edit().setRequired(required); + } + + @Override + public void setHidden(boolean hidden) + { + if (hidden == isHidden()) + return; + edit().setHidden(hidden); + } + + @Override + public void setShownInDetailsView(boolean shown) + { + if (shown == isShownInDetailsView()) + return; + edit().setShownInDetailsView(shown); + } + + @Override + public void setShownInInsertView(boolean shown) + { + if (shown == isShownInInsertView()) + return; + edit().setShownInInsertView(shown); + } + + @Override + public void setShownInUpdateView(boolean shown) + { + if (shown == isShownInUpdateView()) + return; + edit().setShownInUpdateView(shown); + } + + @Override + public void setShownInLookupView(boolean shown) + { + if (shown == isShownInLookupView()) + return; + edit().setShownInLookupView(shown); + } + + @Override + public void setMeasure(boolean isMeasure) + { + // UNDONE: isMeasure() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isMeasure == isMeasure()) + return; + edit().setMeasure(isMeasure); + } + + @Override + public void setDimension(boolean isDimension) + { + // UNDONE: isDimension() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isDimension == isDimension()) + return; + edit().setDimension(isDimension); + } + + @Override + public void setRecommendedVariable(boolean isRecommendedVariable) + { + if (!isEdited() && isRecommendedVariable == isRecommendedVariable()) + return; + edit().setRecommendedVariable(isRecommendedVariable); + } + + @Override + public void setDefaultScale(DefaultScaleType defaultScale) + { + if (!isEdited() && getDefaultScale() == defaultScale) + return; + + edit().setDefaultScale(defaultScale); + } + + @Override + public void setPhi(PHI phi) + { + if (!isEdited() && getPHI() == phi) + return; + edit().setPHI(phi); + } + + @Override + public void setRedactedText(String redactedText) + { + if (!isEdited() && ((getRedactedText() != null && getRedactedText().equals(redactedText)) + || (getRedactedText() == null && redactedText == null))) + return; + edit().setRedactedText(redactedText); + } + + @Override + public void setExcludeFromShifting(boolean isExcludeFromShifting) + { + // UNDONE: isExcludeFromShifting() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isExcludeFromShifting == isExcludeFromShifting()) + return; + edit().setExcludeFromShifting(isExcludeFromShifting); + } + + @Override + public void setMvEnabled(boolean mv) + { + if (mv == isMvEnabled()) + return; + edit().setMvEnabled(mv); + } + + @Override + public void setScale(int scale) + { + if (scale == getScale()) + return; + edit().setScale(scale); + } + + /** Need the string version of this method because it's called by reflection and must match by name */ + public void setImportAliases(String aliases) + { + if (Strings.CS.equals(aliases, getImportAliases())) + return; + edit().setImportAliases(aliases); + } + + /** Need the string version of this method because it's called by reflection and must match by name */ + public String getImportAliases() + { + return _pd.getImportAliases(); + } + + @Override + public void setImportAliasSet(Set aliases) + { + String current = getImportAliases(); + String newAliases = ColumnRenderPropertiesImpl.convertToString(aliases); + if (Strings.CS.equals(current, newAliases)) + return; + edit().setImportAliasesSet(aliases); + } + + @Override + public Set getImportAliasSet() + { + return _pd.getImportAliasSet(); + } + + @Override + public void setURL(String url) + { + if (Strings.CS.equals(getURL(), url)) + return; + + if (null == url) + edit().setURL(null); + else + edit().setURL(StringExpressionFactory.createURL(url)); + } + + @Override + public String getURL() + { + return _pd.getURL() == null ? null : _pd.getURL().toString(); + } + + @Override + public void setURLTarget(String urlTarget) + { + if (Strings.CS.equals(getURLTarget(), urlTarget)) + return; + edit().setURLTarget(urlTarget); + } + + @Override + public String getURLTarget() + { + return _pd.getURLTarget(); + } + + private boolean isEdited() + { + return null != _pdOld; + } + + private PropertyDescriptor editSchema() + { + PropertyDescriptor pd = edit(); + _schemaChanged = true; + _pd.clearPropertyType(); + return pd; + } + + public boolean isRecreateRequired() + { + return _schemaChanged && _schemaImport; + } + + public void markAsNew() + { + assert isRecreateRequired() && !isNew(); + _pd.setPropertyId(0); + } + + private PropertyDescriptor edit() + { + if (_pdOld == null) + { + _pdOld = _pd; + _pd = _pdOld.clone(); + } + return _pd; + } + + @Override + public PropertyType getPropertyType() + { + return _pd.getPropertyType(); + } + + @Override + public JdbcType getJdbcType() + { + return _pd.getPropertyType().getJdbcType(); + } + + @Override + public int getScale() + { + return _pd.getScale(); + } + + @Override + public String getInputType() + { + return _pd.getPropertyType().getInputType(); + } + + @Override + public DefaultValueType getDefaultValueTypeEnum() + { + return _pd.getDefaultValueTypeEnum(); + } + + @Override + public void setDefaultValueTypeEnum(DefaultValueType defaultValueType) + { + _pd.setDefaultValueTypeEnum(defaultValueType); + } + + public String getDefaultValueType() + { + return _pd.getDefaultValueType(); + } + + @Override + public void setDefaultValueType(String defaultValueTypeName) + { + if (getDefaultValueType() != null && getDefaultValueType().equals(defaultValueTypeName)) + return; + + if (getDefaultValueType() == null && defaultValueTypeName == null) + return; // if both are null, don't call edit(), with marks property as dirty + + edit().setDefaultValueType(defaultValueTypeName); + } + + @Override + public void setDefaultValue(String value) + { + _defaultValue = value; + } + + public String getDefaultValue() + { + return _defaultValue; + } + + @Override + public Lookup getLookup() + { + return _pd.getLookup(); + } + + @Override + public void setLookup(Lookup lookup) + { + Lookup current = getLookup(); + + if (current == lookup) + return; + + // current will return null if the schema or query is null so check + // for this case in the passed in lookup + if (current == null) + if (lookup.getQueryName() == null || lookup.getSchemaKey() == null) + return; + + if (current != null && current.equals(lookup)) + return; + + if (lookup == null) + { + edit().setLookupContainer(null); + edit().setLookupSchema(null); + edit().setLookupQuery(null); + return; + } + if (lookup.getContainer() == null) + { + edit().setLookupContainer(null); + } + else + { + edit().setLookupContainer(lookup.getContainer().getId()); + } + edit().setLookupQuery(lookup.getQueryName()); + edit().setLookupSchema(Objects.toString(lookup.getSchemaKey(),null)); + } + + @Override + public void setScannable(boolean scannable) + { + if (scannable != isScannable()) + edit().setScannable(scannable); + } + + @Override + public void setOldPropertyDescriptor(PropertyDescriptor oldPropertyDescriptor) + { + if (isEdited()) + return; + + _pdOld = oldPropertyDescriptor.clone(); + } + + @Override + public boolean isScannable() + { + return _pd.isScannable(); + } + + @Override + public void setPrincipalConceptCode(String code) + { + if (!Strings.CS.equals(code, getPrincipalConceptCode())) + edit().setPrincipalConceptCode(code); + } + + @Override + public String getPrincipalConceptCode() + { + return _pd.getPrincipalConceptCode(); + } + + @Override + public String getSourceOntology() + { + return _pd.getSourceOntology(); + } + + @Override + public void setSourceOntology(String sourceOntology) + { + if (!Strings.CS.equals(sourceOntology, getSourceOntology())) + edit().setSourceOntology(sourceOntology); + } + + @Override + public String getConceptSubtree() + { + return _pd.getConceptSubtree(); + } + + @Override + public void setConceptSubtree(String path) + { + if (!Strings.CS.equals(path, getConceptSubtree())) + edit().setConceptSubtree(path); + } + + @Override + public String getConceptImportColumn() + { + return _pd.getConceptImportColumn(); + } + + @Override + public void setConceptImportColumn(String conceptImportColumn) + { + if (!Strings.CS.equals(conceptImportColumn, getConceptImportColumn())) + edit().setConceptImportColumn(conceptImportColumn); + } + + @Override + public String getConceptLabelColumn() + { + return _pd.getConceptLabelColumn(); + } + + @Override + public void setConceptLabelColumn(String conceptLabelColumn) + { + if (!Strings.CS.equals(conceptLabelColumn, getConceptLabelColumn())) + edit().setConceptLabelColumn(conceptLabelColumn); + } + + @Override + public void setDerivationDataScope(String scope) + { + if (!Strings.CS.equals(scope, getDerivationDataScope())) + edit().setDerivationDataScope(scope); + } + + @Override + public String getDerivationDataScope() + { + return _pd.getDerivationDataScope(); + } + + @Override + public PropertyDescriptor getPropertyDescriptor() + { + return _pd; + } + + @Override + public List getConditionalFormats() + { + return ensureConditionalFormats(); + } + + public boolean isNew() + { + return _pd.getPropertyId() == 0; + } + + // Scenario to swap property descriptors on study upload to or from a system property, instead of updating the + // current property descriptor. Avoids overwriting a system property. + public boolean isSystemPropertySwap() + { + if (_pd.getPropertyId() == 0 && _pd.getPropertyURI() != null && _pdOld != null && _pdOld.getPropertyURI() != null + && !_pd.getPropertyURI().equals(_pdOld.getPropertyURI())) + { + return SystemProperty.getProperties().stream().anyMatch(sp -> + sp.getPropertyURI().equals(_pd.getPropertyURI()) || sp.getPropertyURI().equals(_pdOld.getPropertyURI())); + } + + return false; + } + + public boolean isDirty() + { + if (_pdOld != null) return true; + + for (PropertyValidatorImpl v : ensureValidators()) + { + if (v.isDirty() || v.isNew()) + return true; + } + return false; + } + + public void delete(User user) + { + DomainPropertyManager.get().removeValidatorsForPropertyDescriptor(getContainer(), getPropertyId()); + DomainPropertyManager.get().deleteConditionalFormats(getPropertyId()); + + DomainKind kind = getDomain().getDomainKind(); + if (null != kind) + kind.deletePropertyDescriptor(getDomain(), user, _pd); + OntologyManager.removePropertyDescriptorFromDomain(this); + } + + public void save(User user, DomainDescriptor dd, int sortOrder) throws ChangePropertyDescriptorException + { + if (isSystemPropertySwap()) + { + _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); + OntologyManager.removePropertyDescriptorFromDomain(new DomainPropertyImpl((DomainImpl) getDomain(), _pdOld)); + } + else if (isNew()) + { + _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); + } + else if (_pdOld != null) + { + PropertyType oldType = _pdOld.getPropertyType(); + PropertyType newType = _pd.getPropertyType(); + boolean changedType = false; + if (oldType.getJdbcType() != newType.getJdbcType()) + { + if (newType.getJdbcType().isText() || + (oldType.getJdbcType().isInteger() && newType.getJdbcType().isNumeric())) + { + changedType = true; + if (newType.getJdbcType().isText()) + { + // Remove any previously set formatting string as it won't apply to a text field + _pd.setFormat(null); + } + } + else if (newType.getJdbcType().isDateOrTime() && oldType.getJdbcType().isDateOrTime()) + { + changedType = true; + _pd.setFormat(null); + } + else if (newType == PropertyType.MULTI_CHOICE || oldType == PropertyType.MULTI_CHOICE) + { + changedType = true; + _pd.setFormat(null); + } + else + { + throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.getJdbcType() + " to " + newType.getJdbcType() + "."); + } + } + + // Issue 44711: Prevent attachment and file field types from being converted to a different type + if (PropertyType.FILE_LINK.getInputType().equalsIgnoreCase(oldType.getInputType()) && oldType != newType) + throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.name() + " to " + newType.name() + "."); + + // GitHub Issue 951: Multi-line values converted to text choices lose multi-line editability + if (oldType == PropertyType.MULTI_LINE && + (PropertyType.MULTI_CHOICE == newType ||TEXT_CHOICE_CONCEPT_URI.equals(_pd.getConceptURI()))) + { + throw new ChangePropertyDescriptorException("Cannot convert a multiline text field to a text choice field."); + } + + OntologyManager.validatePropertyDescriptor(_pd); + Table.update(user, OntologyManager.getTinfoPropertyDescriptor(), _pd, _pdOld.getPropertyId()); + OntologyManager.ensurePropertyDomain(_pd, dd, sortOrder); + + boolean hasProvisioner = null != getDomain().getDomainKind() && null != getDomain().getDomainKind().getStorageSchemaName() && dd.getStorageTableName() != null; + SqlDialect dialect = OntologyManager.getExpSchema().getSqlDialect(); + + if (hasProvisioner) + { + boolean mvAdded = !_pdOld.isMvEnabled() && _pd.isMvEnabled(); + boolean mvDropped = _pdOld.isMvEnabled() && !_pd.isMvEnabled(); + boolean propRenamed = !_pdOld.getName().equals(_pd.getName()); + boolean propResized = _pd.isStringType() && _pdOld.getScale() != _pd.getScale(); + + // Drop first, so rename doesn't have to worry about it + if (mvDropped) + ((StorageProvisionerImpl)StorageProvisioner.get()).dropMvIndicator(this, _pdOld); + + if (propRenamed) + StorageProvisionerImpl.get().renameProperty(this.getDomain(), this, _pdOld, mvDropped); + + if (changedType) + { + var domainKind = _domain.getDomainKind(); + if (domainKind == null) + throw new ChangePropertyDescriptorException("Cannot change property type for domain, unknown domain kind."); + + StorageProvisionerImpl.get().changePropertyType(this.getDomain(), this); + if (_pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) + { + updateBooleanValue( + new SQLFragment().appendIdentifier(domainKind.getStorageSchemaName()).append(".").appendIdentifier(_domain.getStorageTableName()), + _pd.getLegalSelectName(dialect), _pdOld.getFormat(), null); // GitHub Issue #647 + } + + TableInfo table = domainKind.getTableInfo(user, getContainer(), _domain, ContainerFilter.getUnsafeEverythingFilter()); + if (table != null && _pdOld.getPropertyType() != null && table.getSchema().getSqlDialect().isPostgreSQL()) + QueryChangeListener.QueryPropertyChange.handleColumnTypeChange(_pdOld, _pd, SchemaKey.fromString(table.getUserSchema().getSchemaName()), table.getName(), user, getContainer()); + } + else if (propResized) + StorageProvisionerImpl.get().resizeProperty(this.getDomain(), this, _pdOld.getScale()); + + if (mvAdded) + StorageProvisionerImpl.get().addMvIndicator(this); + } + else if (changedType) + { + if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isText()) + { + new SqlExecutor(OntologyManager.getExpSchema()).execute( + new SQLFragment("UPDATE "). + append(OntologyManager.getTinfoObjectProperty()). + append(" SET StringValue = DateTimeValue, DateTimeValue = NULL WHERE PropertyId = ?"). + add(_pdOld.getPropertyId())); + } + else if (!oldType.getJdbcType().isText() && newType.getJdbcType().isText()) + { + new SqlExecutor(OntologyManager.getExpSchema()).execute( + new SQLFragment("UPDATE "). + append(OntologyManager.getTinfoObjectProperty()). + append(" SET StringValue = FloatValue, FloatValue = NULL WHERE PropertyId = ?"). + add(_pdOld.getPropertyId())); + } + else if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isDateOrTime()) + { + String sqlTypeName = dialect.getSqlTypeName(newType.getJdbcType()); + String update = String.format("CAST(DateTimeValue AS %s)", sqlTypeName); + if (newType.getJdbcType() == JdbcType.TIME) + update = dialect.getDateTimeToTimeCast("DateTimeValue"); + SQLFragment sqlFragment = new SQLFragment("UPDATE ") + .append(OntologyManager.getTinfoObjectProperty()) + .append(" SET DateTimeValue = ") + .append(update) + .append(" WHERE PropertyId = ?") + .add(_pdOld.getPropertyId()); + new SqlExecutor(OntologyManager.getExpSchema()).execute(sqlFragment); + } + else //noinspection StatementWithEmptyBody + if (oldType.getJdbcType().isInteger() && newType.getJdbcType().isReal()) + { + // Since exp.ObjectProperty stores these types in the same column, there's nothing for us to do + } + else + { + throw new ChangePropertyDescriptorException("Cannot convert from " + oldType.getJdbcType() + " to " + newType.getJdbcType() + " for non-provisioned table"); + } + } + + if (changedType && _pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) + { + updateBooleanValue(OntologyManager.getTinfoObjectProperty().getSQLName(), dialect.makeDatabaseIdentifier("StringValue"), _pdOld.getFormat(), new SQLFragment("PropertyId = ?", _pdOld.getPropertyId())); + } + } + else + { + OntologyManager.ensurePropertyDomain(_pd, _domain._dd, sortOrder); + } + + _pdOld = null; + _schemaChanged = false; + _schemaImport = false; + + for (PropertyValidatorImpl validator : ensureValidators()) + { + if (validator.isDeleted()) + DomainPropertyManager.get().removePropertyValidator(this, validator); + else + DomainPropertyManager.get().savePropertyValidator(user, this, validator); + } + + DomainPropertyManager.get().saveConditionalFormats(user, getPropertyDescriptor(), ensureConditionalFormats()); + } + + /** + * Format values in columns that were just converted from booleans to strings with the DB's default type conversion. + * Postgres will now have 'true' and 'false', and SQLServer will have '0' and '1'. Use the format string to use the + * preferred format, and standardize on 'true' and 'false' in the absence of an explicitly configured format. + */ + private void updateBooleanValue(SQLFragment schemaTable, DatabaseIdentifier column, String formatString, @Nullable SQLFragment whereClause) + { + BooleanFormat f = BooleanFormat.getInstance(formatString); + String trueValue = StringUtils.trimToNull(f.format(true)); + String falseValue = StringUtils.trimToNull(f.format(false)); + String nullValue = StringUtils.trimToNull(f.format(null)); + SQLFragment sql = new SQLFragment("UPDATE ").append(schemaTable).append(" SET "). + appendIdentifier(column).append(" = CASE WHEN "). + appendIdentifier(column).append(" IN ('1', 'true') THEN ? WHEN "). + appendIdentifier(column).append(" IN ('0', 'false') THEN ? ELSE ? END"); + sql.add(trueValue); + sql.add(falseValue); + sql.add(nullValue); + if (whereClause != null) + { + sql.append(" WHERE "); + sql.append(whereClause); + } + new SqlExecutor(OntologyManager.getExpSchema()).execute(sql); + } + + @Override + @NotNull + public List getValidators() + { + return Collections.unmodifiableList(ensureValidators()); + } + + @Override + public void addValidator(IPropertyValidator validator) + { + if (validator != null) + { + if (0 != validator.getPropertyId() && getPropertyId() != validator.getPropertyId()) + throw new IllegalStateException(); + + // Ensure validator is a valid kind (ex. urn:lsid:labkey.com:PropertyValidator:length is no longer valid) + if ( null != PropertyService.get().getValidatorKind(validator.getTypeURI()) ) + { + PropertyValidator impl = new PropertyValidator(); + impl.copy(validator); + impl.setPropertyId(getPropertyId()); + ensureValidators().add(new PropertyValidatorImpl(impl)); + } + } + } + + @Override + public void removeValidator(IPropertyValidator validator) + { + int idx = ensureValidators().indexOf(validator); + if (idx != -1) + { + PropertyValidatorImpl impl = ensureValidators().get(idx); + impl.delete(); + } + } + + @Override + public void removeValidator(long validatorId) + { + if (validatorId == 0) return; + + for (PropertyValidatorImpl imp : ensureValidators()) + { + if (imp.getRowId() == validatorId) + { + imp.delete(); + break; + } + } + } + + @Override + public void copyFrom(DomainProperty propSrc, Container targetContainer) + { + setDescription(propSrc.getDescription()); + setFormat(propSrc.getFormat()); + setLabel(propSrc.getLabel()); + setName(propSrc.getName()); + setDescription(propSrc.getDescription()); + setConceptURI(propSrc.getConceptURI()); + setType(propSrc.getType()); + setDimension(propSrc.isDimension()); + setMeasure(propSrc.isMeasure()); + setRecommendedVariable(propSrc.isRecommendedVariable()); + setDefaultScale(propSrc.getDefaultScale()); + setRequired(propSrc.isRequired()); + setExcludeFromShifting(propSrc.isExcludeFromShifting()); + setFacetingBehavior(propSrc.getFacetingBehavior()); + setImportAliasSet(propSrc.getImportAliasSet()); + setPhi(propSrc.getPHI()); + setURL(propSrc.getURL()); + setURLTarget(propSrc.getURLTarget()); + setHidden(propSrc.isHidden()); + setShownInDetailsView(propSrc.isShownInDetailsView()); + setShownInInsertView(propSrc.isShownInInsertView()); + setShownInUpdateView(propSrc.isShownInUpdateView()); + setShownInLookupView(propSrc.isShownInLookupView()); + setMvEnabled(propSrc.isMvEnabled()); + setDefaultValueTypeEnum(propSrc.getDefaultValueTypeEnum()); + setScale(propSrc.getScale()); + setScannable(propSrc.isScannable()); + + setPrincipalConceptCode(propSrc.getPrincipalConceptCode()); + setSourceOntology(propSrc.getSourceOntology()); + setConceptSubtree(propSrc.getConceptSubtree()); + setConceptImportColumn(propSrc.getConceptImportColumn()); + setConceptLabelColumn(propSrc.getConceptLabelColumn()); + setDerivationDataScope(propSrc.getDerivationDataScope()); + + // check to see if we're moving a lookup column to another container: + Lookup lookup = propSrc.getLookup(); + if (lookup != null && !getContainer().equals(targetContainer)) + { + // we need to update the lookup properties if the lookup container is either the source or the destination container + if (lookup.getContainer() == null) + lookup.setContainer(propSrc.getContainer()); + else if (lookup.getContainer().equals(targetContainer)) + lookup.setContainer(null); + } + setLookup(lookup); + } + + @Override + public void setConditionalFormats(List formats) + { + String newVal = ConditionalFormat.toStringVal(formats); + String oldVal = ConditionalFormat.toStringVal(getConditionalFormats()); + + if (!Objects.equals(newVal, oldVal)) + edit(); + + _formats = formats; + } + + private List ensureValidators() + { + if (_validators == null) + { + _validators = new ArrayList<>(); + for (PropertyValidator validator : DomainPropertyManager.get().getValidators(this)) + { + _validators.add(new PropertyValidatorImpl(validator)); + } + } + return _validators; + } + + private List ensureConditionalFormats() + { + if (_formats == null) + { + _formats = new ArrayList<>(); + _formats.addAll(DomainPropertyManager.get().getConditionalFormats(this)); + } + return _formats; + } + + public PropertyDescriptor getOldProperty() + { + return _pdOld; + } + + @Override + public FacetingBehaviorType getFacetingBehavior() + { + return _pd.getFacetingBehaviorType(); + } + + @Override + public void setFacetingBehavior(FacetingBehaviorType type) + { + if (getFacetingBehavior() == type) + return; + + edit().setFacetingBehaviorType(type); + } + + @Override + public int hashCode() + { + return _pd.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + if (!(obj instanceof DomainPropertyImpl)) + return false; + // once a domain property has been edited, it no longer equals any other domain property: + if (_pdOld != null || ((DomainPropertyImpl) obj)._pdOld != null) + return false; + return (_pd.equals(((DomainPropertyImpl) obj)._pd)); + } + + @Override + public String toString() + { + return super.toString() + _pd.getPropertyURI(); + } + + public Map getAuditRecordMap(@Nullable String validatorStr, @Nullable String conditionalFormatStr) + { + Map map = new LinkedHashMap<>(); + if (!StringUtils.isEmpty(getName())) + map.put("Name", getName()); + if (!StringUtils.isEmpty(getLabel())) + map.put("Label", getLabel()); + if (null != getPropertyType()) + map.put("Type", getPropertyType().getXarName()); + if (getPropertyType().getJdbcType().isText()) + map.put("Scale", getScale()); + if (!StringUtils.isEmpty(getDescription())) + map.put("Description", getDescription()); + if (!StringUtils.isEmpty(getFormat())) + map.put("Format", getFormat()); + if (!StringUtils.isEmpty(getURL())) + map.put("URL", getURL()); + if (!StringUtils.isEmpty(getURLTarget())) + map.put("URLTarget", getURLTarget()); + if (getPHI() != null) + map.put("PHI", getPHI().getLabel()); + if (getDefaultScale() != null) + map.put("DefaultScale", getDefaultScale().getLabel()); + map.put("Required", isRequired()); + map.put("Hidden", isHidden()); + map.put("MvEnabled", isMvEnabled()); + map.put("Measure", isMeasure()); + map.put("Dimension", isDimension()); + map.put("ShownInInsert", isShownInInsertView()); + map.put("ShownInDetails", isShownInDetailsView()); + map.put("ShownInUpdate", isShownInUpdateView()); + map.put("ShownInLookupView", isShownInLookupView()); + map.put("RecommendedVariable", isRecommendedVariable()); + map.put("ExcludedFromShifting", isExcludeFromShifting()); + map.put("Scannable", isScannable()); + if (!StringUtils.isEmpty(getDerivationDataScope())) + map.put("DerivationDataScope", getDerivationDataScope()); + String importAliasStr = StringUtils.join(getImportAliasSet(), ","); + if (!StringUtils.isEmpty(importAliasStr)) + map.put("ImportAliases", importAliasStr); + if (getDefaultValueTypeEnum() != null) + map.put("DefaultValueType", getDefaultValueTypeEnum().getLabel()); + if (getLookup() != null) + map.put("Lookup", getLookup().toJSONString()); + + if (!StringUtils.isEmpty(validatorStr)) + map.put("Validator", validatorStr); + if (!StringUtils.isEmpty(conditionalFormatStr)) + map.put("ConditionalFormat", conditionalFormatStr); + + return map; + } + + public static class TestCase extends Assert + { + private PropertyDescriptor _pd; + private DomainPropertyImpl _dp; + + @Test + public void testUpdateDomainPropertyFromDescriptor() + { + Container c = ContainerManager.ensureContainer("/_DomainPropertyImplTest", TestContext.get().getUser()); + String domainURI = new Lsid("Junit", "DD", "Domain1").toString(); + Domain d = PropertyService.get().createDomain(c, domainURI, "Domain1"); + + resetProperties(d, domainURI, c); + + // verify no change + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertFalse(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + + // change a property + _pd.setPHI(PHI.Restricted); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + assertTrue(_dp.getPHI() == _pd.getPHI()); + + // Issue #18738 change the schema outside of a schema reload and verify that the column + // change the schema but don't mark the property as "Schema Import" + // this will allow whatever type changes the UI allows (text -> multiline, for example) + resetProperties(d, domainURI, c); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertTrue(_dp._schemaChanged); + assertFalse(_dp.isRecreateRequired()); + assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); + + // setting schema import to true will enable the _schemaChanged flag to toggle + // so it should be set true here + resetProperties(d, domainURI, c); + _dp.setSchemaImport(true); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertTrue(_dp._schemaChanged); + assertTrue(_dp.isRecreateRequired()); + assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); + + // verify no change when setting value to the same value as it was + resetProperties(d, domainURI, c); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); + _pd.setPHI(PHI.NotPHI); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertFalse(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + assertFalse(_dp.isRecreateRequired()); + + // verify Lookup is set to null with null schema + resetProperties(d, domainURI, c); + verifyLookup(null, "lkSchema", null, true); + + // verify Lookup is set to null with null query + resetProperties(d, domainURI, c); + verifyLookup(null, null, "lkQuery",true); + + // verify Lookup is set to null with invalid container + resetProperties(d, domainURI, c); + verifyLookup("bogus", null, "lkQuery",true); + + // verify Lookup is set with valid schema and query + resetProperties(d, domainURI, c); + verifyLookup(null, "lkSchema", "lkQuery",true); + + // verify Lookup is set with valid container, schema and query + resetProperties(d, domainURI, c); + verifyLookup(c.getId(), "lkSchema1", "lkQuery2",true); + + // no cleanup as we never persisted anything + } + + private void verifyLookup(String containerId, String schema, String query, Boolean expectedDirty) + { + _pd.setLookupContainer(containerId); + _pd.setLookupQuery(query); + _pd.setLookupSchema(schema); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty() == expectedDirty); + assertFalse(_dp._schemaChanged); + + // verify the lookup object returned + Lookup l = _dp.getLookup(); + + if (l == null) + { + // lookup can be null if we specified a containerId that is invalid or + // we specified a valid containerId (including null) but schema or query is null + if (containerId != null && null == ContainerManager.getForId(containerId)) + assertTrue(true); + else if (query == null || schema == null) + assertTrue(true); + else + assertTrue(false); + } + else + { + if (containerId != null) + assertTrue(Strings.CS.equals(l.getContainer().getId(), _pd.getLookupContainer())); + + assertTrue(Strings.CS.equals(l.getQueryName(), _pd.getLookupQuery())); + assertTrue(Strings.CS.equals(l.getSchemaKey().toString(), _pd.getLookupSchema())); + } + } + + private void resetProperties(Domain d, String domainUri, Container c) + { + _pd = getPropertyDescriptor(c, domainUri); + _dp = (DomainPropertyImpl) d.addProperty(); + _pd.copyTo(_dp.getPropertyDescriptor()); + } + + + private PropertyDescriptor getPropertyDescriptor(Container c, String domainURI) + { + PropertyDescriptor pd = new PropertyDescriptor(); + pd.setPropertyURI(domainURI + ":column"); + pd.setName("column"); + pd.setLabel("label"); + pd.setConceptURI(null); + pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); + pd.setContainer(c); + pd.setDescription("description"); + pd.setURL(StringExpressionFactory.createURL((String)null)); + pd.setURLTarget(null); + pd.setImportAliases(null); + pd.setRequired(false); + pd.setHidden(false); + pd.setShownInInsertView(true); + pd.setShownInUpdateView(true); + pd.setShownInDetailsView(true); + pd.setDimension(false); + pd.setMeasure(true); + pd.setRecommendedVariable(false); + pd.setDefaultScale(DefaultScaleType.LINEAR); + pd.setFormat(null); + pd.setMvEnabled(false); + pd.setLookupContainer(c.getId()); + pd.setLookupSchema("lkSchema"); + pd.setLookupQuery("lkQuery"); + pd.setFacetingBehaviorType(FacetingBehaviorType.AUTOMATIC); + pd.setPHI(PHI.NotPHI); + pd.setExcludeFromShifting(false); + return pd; + } + } + + +} From de8e15fedb980a7355accb90d918bd2d1636e618 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 31 Mar 2026 20:10:13 -0700 Subject: [PATCH 2/4] crlf --- .../api/property/DomainPropertyImpl.java | 2804 ++++++++--------- 1 file changed, 1402 insertions(+), 1402 deletions(-) diff --git a/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java b/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java index d3c01332986..af7734719c9 100644 --- a/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java +++ b/experiment/src/org/labkey/experiment/api/property/DomainPropertyImpl.java @@ -1,1402 +1,1402 @@ -/* - * Copyright (c) 2008-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.labkey.experiment.api.property; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Strings; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.junit.Assert; -import org.junit.Test; -import org.labkey.api.data.BooleanFormat; -import org.labkey.api.data.ColumnRenderPropertiesImpl; -import org.labkey.api.data.ConditionalFormat; -import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerFilter; -import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.DatabaseIdentifier; -import org.labkey.api.data.JdbcType; -import org.labkey.api.data.PHI; -import org.labkey.api.data.SQLFragment; -import org.labkey.api.data.SqlExecutor; -import org.labkey.api.data.Table; -import org.labkey.api.data.TableInfo; -import org.labkey.api.data.dialect.SqlDialect; -import org.labkey.api.exp.ChangePropertyDescriptorException; -import org.labkey.api.exp.DomainDescriptor; -import org.labkey.api.exp.Lsid; -import org.labkey.api.exp.OntologyManager; -import org.labkey.api.exp.PropertyDescriptor; -import org.labkey.api.exp.PropertyType; -import org.labkey.api.exp.api.StorageProvisioner; -import org.labkey.api.exp.property.Domain; -import org.labkey.api.exp.property.DomainKind; -import org.labkey.api.exp.property.DomainProperty; -import org.labkey.api.exp.property.IPropertyType; -import org.labkey.api.exp.property.IPropertyValidator; -import org.labkey.api.exp.property.Lookup; -import org.labkey.api.exp.property.PropertyService; -import org.labkey.api.exp.property.SystemProperty; -import org.labkey.api.gwt.client.DefaultScaleType; -import org.labkey.api.gwt.client.DefaultValueType; -import org.labkey.api.gwt.client.FacetingBehaviorType; -import org.labkey.api.query.QueryChangeListener; -import org.labkey.api.query.SchemaKey; -import org.labkey.api.security.User; -import org.labkey.api.util.StringExpressionFactory; -import org.labkey.api.util.TestContext; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import static org.labkey.api.data.ColumnRenderPropertiesImpl.TEXT_CHOICE_CONCEPT_URI; - -public class DomainPropertyImpl implements DomainProperty -{ - private final DomainImpl _domain; - - PropertyDescriptor _pd; - PropertyDescriptor _pdOld; - boolean _deleted; - - private boolean _schemaChanged; - private boolean _schemaImport; - private List _validators; - private List _formats; - private String _defaultValue; - - public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd) - { - this(type, pd, null); - } - - public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd, List formats) - { - _domain = type; - _pd = pd.clone(); - _formats = formats; - } - - @Override - public int getPropertyId() - { - return _pd.getPropertyId(); - } - - @Override - public Container getContainer() - { - return _pd.getContainer(); - } - - @Override - public String getPropertyURI() - { - return _pd.getPropertyURI(); - } - - @Override - public String getName() - { - return _pd.getName(); - } - - @Override - public String getDescription() - { - return _pd.getDescription(); - } - - @Override - public String getFormat() - { - return _pd.getFormat(); - } - - @Override - public String getLabel() - { - return _pd.getLabel(); - } - - @Override - public String getConceptURI() - { - return _pd.getConceptURI(); - } - - @Override - public Domain getDomain() - { - return _domain; - } - - @Override - public IPropertyType getType() - { - return PropertyService.get().getType(getContainer(), _pd.getRangeURI()); - } - - @Override - public boolean isRequired() - { - return _pd.isRequired(); - } - - @Override - public boolean isHidden() - { - return _pd.isHidden(); - } - - @Override - public boolean isDeleted() - { - return _deleted; - } - - @Override - public boolean isShownInInsertView() - { - return _pd.isShownInInsertView(); - } - - @Override - public boolean isShownInDetailsView() - { - return _pd.isShownInDetailsView(); - } - - @Override - public boolean isShownInUpdateView() - { - return _pd.isShownInUpdateView(); - } - - @Override - public boolean isShownInLookupView() - { - return _pd.isShownInLookupView(); - } - - @Override - public boolean isMeasure() - { - return _pd.isMeasure(); - } - - @Override - public boolean isDimension() - { - return _pd.isDimension(); - } - - @Override - public boolean isRecommendedVariable() - { - return _pd.isRecommendedVariable(); - } - - @Override - public DefaultScaleType getDefaultScale() - { - return _pd.getDefaultScale(); - } - - @Override - public PHI getPHI() - { - return _pd.getPHI(); - } - - @Override - public String getRedactedText() { return _pd.getRedactedText(); } - - @Override - public boolean isExcludeFromShifting() - { - return _pd.isExcludeFromShifting(); - } - - @Override - public boolean isMvEnabled() - { - return _pd.isMvEnabled(); - } - - @Override - public boolean isMvEnabledForDrop() - { - if (null != _pdOld) - return _pdOld.isMvEnabled(); // if we need to drop/recreate we care about the old one - return _pd.isMvEnabled(); - } - - @Override - public void delete() - { - _deleted = true; - } - - @Override - public void setSchemaImport(boolean isSchemaImport) - { - // if this flag is set True then the column is dropped and recreated by its Domain if there is a type change - _schemaImport = isSchemaImport; - } - - @Override - public void setName(String name) - { - if (Strings.CS.equals(name, getName())) - return; - edit().setName(name); - } - - @Override - public void setDescription(String description) - { - if (Strings.CS.equals(description, getDescription())) - return; - edit().setDescription(description); - } - - @Override - public void setType(IPropertyType domain) - { - edit().setRangeURI(domain.getTypeURI()); - } - - @Override - public void setPropertyURI(String uri) - { - if (Strings.CS.equals(uri, getPropertyURI())) - return; - edit().setPropertyURI(uri); - } - - @Override - public void setRangeURI(String rangeURI) - { - if (Strings.CS.equals(rangeURI, getRangeURI())) - return; - editSchema().setRangeURI(rangeURI); - } - - @Override - public String getRangeURI() - { - return _pd.getRangeURI(); - } - - @Override - public void setFormat(String s) - { - if (Strings.CS.equals(s, getFormat())) - return; - edit().setFormat(s); - } - - @Override - public void setLabel(String caption) - { - if (Strings.CS.equals(caption, getLabel())) - return; - edit().setLabel(caption); - } - - @Override - public void setConceptURI(String conceptURI) - { - if (Strings.CS.equals(conceptURI, getConceptURI())) - return; - edit().setConceptURI(conceptURI); - } - - @Override - public void setRequired(boolean required) - { - if (required == isRequired()) - return; - edit().setRequired(required); - } - - @Override - public void setHidden(boolean hidden) - { - if (hidden == isHidden()) - return; - edit().setHidden(hidden); - } - - @Override - public void setShownInDetailsView(boolean shown) - { - if (shown == isShownInDetailsView()) - return; - edit().setShownInDetailsView(shown); - } - - @Override - public void setShownInInsertView(boolean shown) - { - if (shown == isShownInInsertView()) - return; - edit().setShownInInsertView(shown); - } - - @Override - public void setShownInUpdateView(boolean shown) - { - if (shown == isShownInUpdateView()) - return; - edit().setShownInUpdateView(shown); - } - - @Override - public void setShownInLookupView(boolean shown) - { - if (shown == isShownInLookupView()) - return; - edit().setShownInLookupView(shown); - } - - @Override - public void setMeasure(boolean isMeasure) - { - // UNDONE: isMeasure() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isMeasure == isMeasure()) - return; - edit().setMeasure(isMeasure); - } - - @Override - public void setDimension(boolean isDimension) - { - // UNDONE: isDimension() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isDimension == isDimension()) - return; - edit().setDimension(isDimension); - } - - @Override - public void setRecommendedVariable(boolean isRecommendedVariable) - { - if (!isEdited() && isRecommendedVariable == isRecommendedVariable()) - return; - edit().setRecommendedVariable(isRecommendedVariable); - } - - @Override - public void setDefaultScale(DefaultScaleType defaultScale) - { - if (!isEdited() && getDefaultScale() == defaultScale) - return; - - edit().setDefaultScale(defaultScale); - } - - @Override - public void setPhi(PHI phi) - { - if (!isEdited() && getPHI() == phi) - return; - edit().setPHI(phi); - } - - @Override - public void setRedactedText(String redactedText) - { - if (!isEdited() && ((getRedactedText() != null && getRedactedText().equals(redactedText)) - || (getRedactedText() == null && redactedText == null))) - return; - edit().setRedactedText(redactedText); - } - - @Override - public void setExcludeFromShifting(boolean isExcludeFromShifting) - { - // UNDONE: isExcludeFromShifting() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. - if (!isEdited() && isExcludeFromShifting == isExcludeFromShifting()) - return; - edit().setExcludeFromShifting(isExcludeFromShifting); - } - - @Override - public void setMvEnabled(boolean mv) - { - if (mv == isMvEnabled()) - return; - edit().setMvEnabled(mv); - } - - @Override - public void setScale(int scale) - { - if (scale == getScale()) - return; - edit().setScale(scale); - } - - /** Need the string version of this method because it's called by reflection and must match by name */ - public void setImportAliases(String aliases) - { - if (Strings.CS.equals(aliases, getImportAliases())) - return; - edit().setImportAliases(aliases); - } - - /** Need the string version of this method because it's called by reflection and must match by name */ - public String getImportAliases() - { - return _pd.getImportAliases(); - } - - @Override - public void setImportAliasSet(Set aliases) - { - String current = getImportAliases(); - String newAliases = ColumnRenderPropertiesImpl.convertToString(aliases); - if (Strings.CS.equals(current, newAliases)) - return; - edit().setImportAliasesSet(aliases); - } - - @Override - public Set getImportAliasSet() - { - return _pd.getImportAliasSet(); - } - - @Override - public void setURL(String url) - { - if (Strings.CS.equals(getURL(), url)) - return; - - if (null == url) - edit().setURL(null); - else - edit().setURL(StringExpressionFactory.createURL(url)); - } - - @Override - public String getURL() - { - return _pd.getURL() == null ? null : _pd.getURL().toString(); - } - - @Override - public void setURLTarget(String urlTarget) - { - if (Strings.CS.equals(getURLTarget(), urlTarget)) - return; - edit().setURLTarget(urlTarget); - } - - @Override - public String getURLTarget() - { - return _pd.getURLTarget(); - } - - private boolean isEdited() - { - return null != _pdOld; - } - - private PropertyDescriptor editSchema() - { - PropertyDescriptor pd = edit(); - _schemaChanged = true; - _pd.clearPropertyType(); - return pd; - } - - public boolean isRecreateRequired() - { - return _schemaChanged && _schemaImport; - } - - public void markAsNew() - { - assert isRecreateRequired() && !isNew(); - _pd.setPropertyId(0); - } - - private PropertyDescriptor edit() - { - if (_pdOld == null) - { - _pdOld = _pd; - _pd = _pdOld.clone(); - } - return _pd; - } - - @Override - public PropertyType getPropertyType() - { - return _pd.getPropertyType(); - } - - @Override - public JdbcType getJdbcType() - { - return _pd.getPropertyType().getJdbcType(); - } - - @Override - public int getScale() - { - return _pd.getScale(); - } - - @Override - public String getInputType() - { - return _pd.getPropertyType().getInputType(); - } - - @Override - public DefaultValueType getDefaultValueTypeEnum() - { - return _pd.getDefaultValueTypeEnum(); - } - - @Override - public void setDefaultValueTypeEnum(DefaultValueType defaultValueType) - { - _pd.setDefaultValueTypeEnum(defaultValueType); - } - - public String getDefaultValueType() - { - return _pd.getDefaultValueType(); - } - - @Override - public void setDefaultValueType(String defaultValueTypeName) - { - if (getDefaultValueType() != null && getDefaultValueType().equals(defaultValueTypeName)) - return; - - if (getDefaultValueType() == null && defaultValueTypeName == null) - return; // if both are null, don't call edit(), with marks property as dirty - - edit().setDefaultValueType(defaultValueTypeName); - } - - @Override - public void setDefaultValue(String value) - { - _defaultValue = value; - } - - public String getDefaultValue() - { - return _defaultValue; - } - - @Override - public Lookup getLookup() - { - return _pd.getLookup(); - } - - @Override - public void setLookup(Lookup lookup) - { - Lookup current = getLookup(); - - if (current == lookup) - return; - - // current will return null if the schema or query is null so check - // for this case in the passed in lookup - if (current == null) - if (lookup.getQueryName() == null || lookup.getSchemaKey() == null) - return; - - if (current != null && current.equals(lookup)) - return; - - if (lookup == null) - { - edit().setLookupContainer(null); - edit().setLookupSchema(null); - edit().setLookupQuery(null); - return; - } - if (lookup.getContainer() == null) - { - edit().setLookupContainer(null); - } - else - { - edit().setLookupContainer(lookup.getContainer().getId()); - } - edit().setLookupQuery(lookup.getQueryName()); - edit().setLookupSchema(Objects.toString(lookup.getSchemaKey(),null)); - } - - @Override - public void setScannable(boolean scannable) - { - if (scannable != isScannable()) - edit().setScannable(scannable); - } - - @Override - public void setOldPropertyDescriptor(PropertyDescriptor oldPropertyDescriptor) - { - if (isEdited()) - return; - - _pdOld = oldPropertyDescriptor.clone(); - } - - @Override - public boolean isScannable() - { - return _pd.isScannable(); - } - - @Override - public void setPrincipalConceptCode(String code) - { - if (!Strings.CS.equals(code, getPrincipalConceptCode())) - edit().setPrincipalConceptCode(code); - } - - @Override - public String getPrincipalConceptCode() - { - return _pd.getPrincipalConceptCode(); - } - - @Override - public String getSourceOntology() - { - return _pd.getSourceOntology(); - } - - @Override - public void setSourceOntology(String sourceOntology) - { - if (!Strings.CS.equals(sourceOntology, getSourceOntology())) - edit().setSourceOntology(sourceOntology); - } - - @Override - public String getConceptSubtree() - { - return _pd.getConceptSubtree(); - } - - @Override - public void setConceptSubtree(String path) - { - if (!Strings.CS.equals(path, getConceptSubtree())) - edit().setConceptSubtree(path); - } - - @Override - public String getConceptImportColumn() - { - return _pd.getConceptImportColumn(); - } - - @Override - public void setConceptImportColumn(String conceptImportColumn) - { - if (!Strings.CS.equals(conceptImportColumn, getConceptImportColumn())) - edit().setConceptImportColumn(conceptImportColumn); - } - - @Override - public String getConceptLabelColumn() - { - return _pd.getConceptLabelColumn(); - } - - @Override - public void setConceptLabelColumn(String conceptLabelColumn) - { - if (!Strings.CS.equals(conceptLabelColumn, getConceptLabelColumn())) - edit().setConceptLabelColumn(conceptLabelColumn); - } - - @Override - public void setDerivationDataScope(String scope) - { - if (!Strings.CS.equals(scope, getDerivationDataScope())) - edit().setDerivationDataScope(scope); - } - - @Override - public String getDerivationDataScope() - { - return _pd.getDerivationDataScope(); - } - - @Override - public PropertyDescriptor getPropertyDescriptor() - { - return _pd; - } - - @Override - public List getConditionalFormats() - { - return ensureConditionalFormats(); - } - - public boolean isNew() - { - return _pd.getPropertyId() == 0; - } - - // Scenario to swap property descriptors on study upload to or from a system property, instead of updating the - // current property descriptor. Avoids overwriting a system property. - public boolean isSystemPropertySwap() - { - if (_pd.getPropertyId() == 0 && _pd.getPropertyURI() != null && _pdOld != null && _pdOld.getPropertyURI() != null - && !_pd.getPropertyURI().equals(_pdOld.getPropertyURI())) - { - return SystemProperty.getProperties().stream().anyMatch(sp -> - sp.getPropertyURI().equals(_pd.getPropertyURI()) || sp.getPropertyURI().equals(_pdOld.getPropertyURI())); - } - - return false; - } - - public boolean isDirty() - { - if (_pdOld != null) return true; - - for (PropertyValidatorImpl v : ensureValidators()) - { - if (v.isDirty() || v.isNew()) - return true; - } - return false; - } - - public void delete(User user) - { - DomainPropertyManager.get().removeValidatorsForPropertyDescriptor(getContainer(), getPropertyId()); - DomainPropertyManager.get().deleteConditionalFormats(getPropertyId()); - - DomainKind kind = getDomain().getDomainKind(); - if (null != kind) - kind.deletePropertyDescriptor(getDomain(), user, _pd); - OntologyManager.removePropertyDescriptorFromDomain(this); - } - - public void save(User user, DomainDescriptor dd, int sortOrder) throws ChangePropertyDescriptorException - { - if (isSystemPropertySwap()) - { - _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); - OntologyManager.removePropertyDescriptorFromDomain(new DomainPropertyImpl((DomainImpl) getDomain(), _pdOld)); - } - else if (isNew()) - { - _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); - } - else if (_pdOld != null) - { - PropertyType oldType = _pdOld.getPropertyType(); - PropertyType newType = _pd.getPropertyType(); - boolean changedType = false; - if (oldType.getJdbcType() != newType.getJdbcType()) - { - if (newType.getJdbcType().isText() || - (oldType.getJdbcType().isInteger() && newType.getJdbcType().isNumeric())) - { - changedType = true; - if (newType.getJdbcType().isText()) - { - // Remove any previously set formatting string as it won't apply to a text field - _pd.setFormat(null); - } - } - else if (newType.getJdbcType().isDateOrTime() && oldType.getJdbcType().isDateOrTime()) - { - changedType = true; - _pd.setFormat(null); - } - else if (newType == PropertyType.MULTI_CHOICE || oldType == PropertyType.MULTI_CHOICE) - { - changedType = true; - _pd.setFormat(null); - } - else - { - throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.getJdbcType() + " to " + newType.getJdbcType() + "."); - } - } - - // Issue 44711: Prevent attachment and file field types from being converted to a different type - if (PropertyType.FILE_LINK.getInputType().equalsIgnoreCase(oldType.getInputType()) && oldType != newType) - throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.name() + " to " + newType.name() + "."); - - // GitHub Issue 951: Multi-line values converted to text choices lose multi-line editability - if (oldType == PropertyType.MULTI_LINE && - (PropertyType.MULTI_CHOICE == newType ||TEXT_CHOICE_CONCEPT_URI.equals(_pd.getConceptURI()))) - { - throw new ChangePropertyDescriptorException("Cannot convert a multiline text field to a text choice field."); - } - - OntologyManager.validatePropertyDescriptor(_pd); - Table.update(user, OntologyManager.getTinfoPropertyDescriptor(), _pd, _pdOld.getPropertyId()); - OntologyManager.ensurePropertyDomain(_pd, dd, sortOrder); - - boolean hasProvisioner = null != getDomain().getDomainKind() && null != getDomain().getDomainKind().getStorageSchemaName() && dd.getStorageTableName() != null; - SqlDialect dialect = OntologyManager.getExpSchema().getSqlDialect(); - - if (hasProvisioner) - { - boolean mvAdded = !_pdOld.isMvEnabled() && _pd.isMvEnabled(); - boolean mvDropped = _pdOld.isMvEnabled() && !_pd.isMvEnabled(); - boolean propRenamed = !_pdOld.getName().equals(_pd.getName()); - boolean propResized = _pd.isStringType() && _pdOld.getScale() != _pd.getScale(); - - // Drop first, so rename doesn't have to worry about it - if (mvDropped) - ((StorageProvisionerImpl)StorageProvisioner.get()).dropMvIndicator(this, _pdOld); - - if (propRenamed) - StorageProvisionerImpl.get().renameProperty(this.getDomain(), this, _pdOld, mvDropped); - - if (changedType) - { - var domainKind = _domain.getDomainKind(); - if (domainKind == null) - throw new ChangePropertyDescriptorException("Cannot change property type for domain, unknown domain kind."); - - StorageProvisionerImpl.get().changePropertyType(this.getDomain(), this); - if (_pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) - { - updateBooleanValue( - new SQLFragment().appendIdentifier(domainKind.getStorageSchemaName()).append(".").appendIdentifier(_domain.getStorageTableName()), - _pd.getLegalSelectName(dialect), _pdOld.getFormat(), null); // GitHub Issue #647 - } - - TableInfo table = domainKind.getTableInfo(user, getContainer(), _domain, ContainerFilter.getUnsafeEverythingFilter()); - if (table != null && _pdOld.getPropertyType() != null && table.getSchema().getSqlDialect().isPostgreSQL()) - QueryChangeListener.QueryPropertyChange.handleColumnTypeChange(_pdOld, _pd, SchemaKey.fromString(table.getUserSchema().getSchemaName()), table.getName(), user, getContainer()); - } - else if (propResized) - StorageProvisionerImpl.get().resizeProperty(this.getDomain(), this, _pdOld.getScale()); - - if (mvAdded) - StorageProvisionerImpl.get().addMvIndicator(this); - } - else if (changedType) - { - if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isText()) - { - new SqlExecutor(OntologyManager.getExpSchema()).execute( - new SQLFragment("UPDATE "). - append(OntologyManager.getTinfoObjectProperty()). - append(" SET StringValue = DateTimeValue, DateTimeValue = NULL WHERE PropertyId = ?"). - add(_pdOld.getPropertyId())); - } - else if (!oldType.getJdbcType().isText() && newType.getJdbcType().isText()) - { - new SqlExecutor(OntologyManager.getExpSchema()).execute( - new SQLFragment("UPDATE "). - append(OntologyManager.getTinfoObjectProperty()). - append(" SET StringValue = FloatValue, FloatValue = NULL WHERE PropertyId = ?"). - add(_pdOld.getPropertyId())); - } - else if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isDateOrTime()) - { - String sqlTypeName = dialect.getSqlTypeName(newType.getJdbcType()); - String update = String.format("CAST(DateTimeValue AS %s)", sqlTypeName); - if (newType.getJdbcType() == JdbcType.TIME) - update = dialect.getDateTimeToTimeCast("DateTimeValue"); - SQLFragment sqlFragment = new SQLFragment("UPDATE ") - .append(OntologyManager.getTinfoObjectProperty()) - .append(" SET DateTimeValue = ") - .append(update) - .append(" WHERE PropertyId = ?") - .add(_pdOld.getPropertyId()); - new SqlExecutor(OntologyManager.getExpSchema()).execute(sqlFragment); - } - else //noinspection StatementWithEmptyBody - if (oldType.getJdbcType().isInteger() && newType.getJdbcType().isReal()) - { - // Since exp.ObjectProperty stores these types in the same column, there's nothing for us to do - } - else - { - throw new ChangePropertyDescriptorException("Cannot convert from " + oldType.getJdbcType() + " to " + newType.getJdbcType() + " for non-provisioned table"); - } - } - - if (changedType && _pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) - { - updateBooleanValue(OntologyManager.getTinfoObjectProperty().getSQLName(), dialect.makeDatabaseIdentifier("StringValue"), _pdOld.getFormat(), new SQLFragment("PropertyId = ?", _pdOld.getPropertyId())); - } - } - else - { - OntologyManager.ensurePropertyDomain(_pd, _domain._dd, sortOrder); - } - - _pdOld = null; - _schemaChanged = false; - _schemaImport = false; - - for (PropertyValidatorImpl validator : ensureValidators()) - { - if (validator.isDeleted()) - DomainPropertyManager.get().removePropertyValidator(this, validator); - else - DomainPropertyManager.get().savePropertyValidator(user, this, validator); - } - - DomainPropertyManager.get().saveConditionalFormats(user, getPropertyDescriptor(), ensureConditionalFormats()); - } - - /** - * Format values in columns that were just converted from booleans to strings with the DB's default type conversion. - * Postgres will now have 'true' and 'false', and SQLServer will have '0' and '1'. Use the format string to use the - * preferred format, and standardize on 'true' and 'false' in the absence of an explicitly configured format. - */ - private void updateBooleanValue(SQLFragment schemaTable, DatabaseIdentifier column, String formatString, @Nullable SQLFragment whereClause) - { - BooleanFormat f = BooleanFormat.getInstance(formatString); - String trueValue = StringUtils.trimToNull(f.format(true)); - String falseValue = StringUtils.trimToNull(f.format(false)); - String nullValue = StringUtils.trimToNull(f.format(null)); - SQLFragment sql = new SQLFragment("UPDATE ").append(schemaTable).append(" SET "). - appendIdentifier(column).append(" = CASE WHEN "). - appendIdentifier(column).append(" IN ('1', 'true') THEN ? WHEN "). - appendIdentifier(column).append(" IN ('0', 'false') THEN ? ELSE ? END"); - sql.add(trueValue); - sql.add(falseValue); - sql.add(nullValue); - if (whereClause != null) - { - sql.append(" WHERE "); - sql.append(whereClause); - } - new SqlExecutor(OntologyManager.getExpSchema()).execute(sql); - } - - @Override - @NotNull - public List getValidators() - { - return Collections.unmodifiableList(ensureValidators()); - } - - @Override - public void addValidator(IPropertyValidator validator) - { - if (validator != null) - { - if (0 != validator.getPropertyId() && getPropertyId() != validator.getPropertyId()) - throw new IllegalStateException(); - - // Ensure validator is a valid kind (ex. urn:lsid:labkey.com:PropertyValidator:length is no longer valid) - if ( null != PropertyService.get().getValidatorKind(validator.getTypeURI()) ) - { - PropertyValidator impl = new PropertyValidator(); - impl.copy(validator); - impl.setPropertyId(getPropertyId()); - ensureValidators().add(new PropertyValidatorImpl(impl)); - } - } - } - - @Override - public void removeValidator(IPropertyValidator validator) - { - int idx = ensureValidators().indexOf(validator); - if (idx != -1) - { - PropertyValidatorImpl impl = ensureValidators().get(idx); - impl.delete(); - } - } - - @Override - public void removeValidator(long validatorId) - { - if (validatorId == 0) return; - - for (PropertyValidatorImpl imp : ensureValidators()) - { - if (imp.getRowId() == validatorId) - { - imp.delete(); - break; - } - } - } - - @Override - public void copyFrom(DomainProperty propSrc, Container targetContainer) - { - setDescription(propSrc.getDescription()); - setFormat(propSrc.getFormat()); - setLabel(propSrc.getLabel()); - setName(propSrc.getName()); - setDescription(propSrc.getDescription()); - setConceptURI(propSrc.getConceptURI()); - setType(propSrc.getType()); - setDimension(propSrc.isDimension()); - setMeasure(propSrc.isMeasure()); - setRecommendedVariable(propSrc.isRecommendedVariable()); - setDefaultScale(propSrc.getDefaultScale()); - setRequired(propSrc.isRequired()); - setExcludeFromShifting(propSrc.isExcludeFromShifting()); - setFacetingBehavior(propSrc.getFacetingBehavior()); - setImportAliasSet(propSrc.getImportAliasSet()); - setPhi(propSrc.getPHI()); - setURL(propSrc.getURL()); - setURLTarget(propSrc.getURLTarget()); - setHidden(propSrc.isHidden()); - setShownInDetailsView(propSrc.isShownInDetailsView()); - setShownInInsertView(propSrc.isShownInInsertView()); - setShownInUpdateView(propSrc.isShownInUpdateView()); - setShownInLookupView(propSrc.isShownInLookupView()); - setMvEnabled(propSrc.isMvEnabled()); - setDefaultValueTypeEnum(propSrc.getDefaultValueTypeEnum()); - setScale(propSrc.getScale()); - setScannable(propSrc.isScannable()); - - setPrincipalConceptCode(propSrc.getPrincipalConceptCode()); - setSourceOntology(propSrc.getSourceOntology()); - setConceptSubtree(propSrc.getConceptSubtree()); - setConceptImportColumn(propSrc.getConceptImportColumn()); - setConceptLabelColumn(propSrc.getConceptLabelColumn()); - setDerivationDataScope(propSrc.getDerivationDataScope()); - - // check to see if we're moving a lookup column to another container: - Lookup lookup = propSrc.getLookup(); - if (lookup != null && !getContainer().equals(targetContainer)) - { - // we need to update the lookup properties if the lookup container is either the source or the destination container - if (lookup.getContainer() == null) - lookup.setContainer(propSrc.getContainer()); - else if (lookup.getContainer().equals(targetContainer)) - lookup.setContainer(null); - } - setLookup(lookup); - } - - @Override - public void setConditionalFormats(List formats) - { - String newVal = ConditionalFormat.toStringVal(formats); - String oldVal = ConditionalFormat.toStringVal(getConditionalFormats()); - - if (!Objects.equals(newVal, oldVal)) - edit(); - - _formats = formats; - } - - private List ensureValidators() - { - if (_validators == null) - { - _validators = new ArrayList<>(); - for (PropertyValidator validator : DomainPropertyManager.get().getValidators(this)) - { - _validators.add(new PropertyValidatorImpl(validator)); - } - } - return _validators; - } - - private List ensureConditionalFormats() - { - if (_formats == null) - { - _formats = new ArrayList<>(); - _formats.addAll(DomainPropertyManager.get().getConditionalFormats(this)); - } - return _formats; - } - - public PropertyDescriptor getOldProperty() - { - return _pdOld; - } - - @Override - public FacetingBehaviorType getFacetingBehavior() - { - return _pd.getFacetingBehaviorType(); - } - - @Override - public void setFacetingBehavior(FacetingBehaviorType type) - { - if (getFacetingBehavior() == type) - return; - - edit().setFacetingBehaviorType(type); - } - - @Override - public int hashCode() - { - return _pd.hashCode(); - } - - @Override - public boolean equals(Object obj) - { - if (obj == this) - return true; - if (!(obj instanceof DomainPropertyImpl)) - return false; - // once a domain property has been edited, it no longer equals any other domain property: - if (_pdOld != null || ((DomainPropertyImpl) obj)._pdOld != null) - return false; - return (_pd.equals(((DomainPropertyImpl) obj)._pd)); - } - - @Override - public String toString() - { - return super.toString() + _pd.getPropertyURI(); - } - - public Map getAuditRecordMap(@Nullable String validatorStr, @Nullable String conditionalFormatStr) - { - Map map = new LinkedHashMap<>(); - if (!StringUtils.isEmpty(getName())) - map.put("Name", getName()); - if (!StringUtils.isEmpty(getLabel())) - map.put("Label", getLabel()); - if (null != getPropertyType()) - map.put("Type", getPropertyType().getXarName()); - if (getPropertyType().getJdbcType().isText()) - map.put("Scale", getScale()); - if (!StringUtils.isEmpty(getDescription())) - map.put("Description", getDescription()); - if (!StringUtils.isEmpty(getFormat())) - map.put("Format", getFormat()); - if (!StringUtils.isEmpty(getURL())) - map.put("URL", getURL()); - if (!StringUtils.isEmpty(getURLTarget())) - map.put("URLTarget", getURLTarget()); - if (getPHI() != null) - map.put("PHI", getPHI().getLabel()); - if (getDefaultScale() != null) - map.put("DefaultScale", getDefaultScale().getLabel()); - map.put("Required", isRequired()); - map.put("Hidden", isHidden()); - map.put("MvEnabled", isMvEnabled()); - map.put("Measure", isMeasure()); - map.put("Dimension", isDimension()); - map.put("ShownInInsert", isShownInInsertView()); - map.put("ShownInDetails", isShownInDetailsView()); - map.put("ShownInUpdate", isShownInUpdateView()); - map.put("ShownInLookupView", isShownInLookupView()); - map.put("RecommendedVariable", isRecommendedVariable()); - map.put("ExcludedFromShifting", isExcludeFromShifting()); - map.put("Scannable", isScannable()); - if (!StringUtils.isEmpty(getDerivationDataScope())) - map.put("DerivationDataScope", getDerivationDataScope()); - String importAliasStr = StringUtils.join(getImportAliasSet(), ","); - if (!StringUtils.isEmpty(importAliasStr)) - map.put("ImportAliases", importAliasStr); - if (getDefaultValueTypeEnum() != null) - map.put("DefaultValueType", getDefaultValueTypeEnum().getLabel()); - if (getLookup() != null) - map.put("Lookup", getLookup().toJSONString()); - - if (!StringUtils.isEmpty(validatorStr)) - map.put("Validator", validatorStr); - if (!StringUtils.isEmpty(conditionalFormatStr)) - map.put("ConditionalFormat", conditionalFormatStr); - - return map; - } - - public static class TestCase extends Assert - { - private PropertyDescriptor _pd; - private DomainPropertyImpl _dp; - - @Test - public void testUpdateDomainPropertyFromDescriptor() - { - Container c = ContainerManager.ensureContainer("/_DomainPropertyImplTest", TestContext.get().getUser()); - String domainURI = new Lsid("Junit", "DD", "Domain1").toString(); - Domain d = PropertyService.get().createDomain(c, domainURI, "Domain1"); - - resetProperties(d, domainURI, c); - - // verify no change - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertFalse(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - - // change a property - _pd.setPHI(PHI.Restricted); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - assertTrue(_dp.getPHI() == _pd.getPHI()); - - // Issue #18738 change the schema outside of a schema reload and verify that the column - // change the schema but don't mark the property as "Schema Import" - // this will allow whatever type changes the UI allows (text -> multiline, for example) - resetProperties(d, domainURI, c); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertTrue(_dp._schemaChanged); - assertFalse(_dp.isRecreateRequired()); - assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); - - // setting schema import to true will enable the _schemaChanged flag to toggle - // so it should be set true here - resetProperties(d, domainURI, c); - _dp.setSchemaImport(true); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty()); - assertTrue(_dp._schemaChanged); - assertTrue(_dp.isRecreateRequired()); - assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); - - // verify no change when setting value to the same value as it was - resetProperties(d, domainURI, c); - _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); - _pd.setPHI(PHI.NotPHI); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertFalse(_dp.isDirty()); - assertFalse(_dp._schemaChanged); - assertFalse(_dp.isRecreateRequired()); - - // verify Lookup is set to null with null schema - resetProperties(d, domainURI, c); - verifyLookup(null, "lkSchema", null, true); - - // verify Lookup is set to null with null query - resetProperties(d, domainURI, c); - verifyLookup(null, null, "lkQuery",true); - - // verify Lookup is set to null with invalid container - resetProperties(d, domainURI, c); - verifyLookup("bogus", null, "lkQuery",true); - - // verify Lookup is set with valid schema and query - resetProperties(d, domainURI, c); - verifyLookup(null, "lkSchema", "lkQuery",true); - - // verify Lookup is set with valid container, schema and query - resetProperties(d, domainURI, c); - verifyLookup(c.getId(), "lkSchema1", "lkQuery2",true); - - // no cleanup as we never persisted anything - } - - private void verifyLookup(String containerId, String schema, String query, Boolean expectedDirty) - { - _pd.setLookupContainer(containerId); - _pd.setLookupQuery(query); - _pd.setLookupSchema(schema); - OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); - assertTrue(_dp.isDirty() == expectedDirty); - assertFalse(_dp._schemaChanged); - - // verify the lookup object returned - Lookup l = _dp.getLookup(); - - if (l == null) - { - // lookup can be null if we specified a containerId that is invalid or - // we specified a valid containerId (including null) but schema or query is null - if (containerId != null && null == ContainerManager.getForId(containerId)) - assertTrue(true); - else if (query == null || schema == null) - assertTrue(true); - else - assertTrue(false); - } - else - { - if (containerId != null) - assertTrue(Strings.CS.equals(l.getContainer().getId(), _pd.getLookupContainer())); - - assertTrue(Strings.CS.equals(l.getQueryName(), _pd.getLookupQuery())); - assertTrue(Strings.CS.equals(l.getSchemaKey().toString(), _pd.getLookupSchema())); - } - } - - private void resetProperties(Domain d, String domainUri, Container c) - { - _pd = getPropertyDescriptor(c, domainUri); - _dp = (DomainPropertyImpl) d.addProperty(); - _pd.copyTo(_dp.getPropertyDescriptor()); - } - - - private PropertyDescriptor getPropertyDescriptor(Container c, String domainURI) - { - PropertyDescriptor pd = new PropertyDescriptor(); - pd.setPropertyURI(domainURI + ":column"); - pd.setName("column"); - pd.setLabel("label"); - pd.setConceptURI(null); - pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); - pd.setContainer(c); - pd.setDescription("description"); - pd.setURL(StringExpressionFactory.createURL((String)null)); - pd.setURLTarget(null); - pd.setImportAliases(null); - pd.setRequired(false); - pd.setHidden(false); - pd.setShownInInsertView(true); - pd.setShownInUpdateView(true); - pd.setShownInDetailsView(true); - pd.setDimension(false); - pd.setMeasure(true); - pd.setRecommendedVariable(false); - pd.setDefaultScale(DefaultScaleType.LINEAR); - pd.setFormat(null); - pd.setMvEnabled(false); - pd.setLookupContainer(c.getId()); - pd.setLookupSchema("lkSchema"); - pd.setLookupQuery("lkQuery"); - pd.setFacetingBehaviorType(FacetingBehaviorType.AUTOMATIC); - pd.setPHI(PHI.NotPHI); - pd.setExcludeFromShifting(false); - return pd; - } - } - - -} +/* + * Copyright (c) 2008-2019 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.labkey.experiment.api.property; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; +import org.junit.Test; +import org.labkey.api.data.BooleanFormat; +import org.labkey.api.data.ColumnRenderPropertiesImpl; +import org.labkey.api.data.ConditionalFormat; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DatabaseIdentifier; +import org.labkey.api.data.JdbcType; +import org.labkey.api.data.PHI; +import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.Table; +import org.labkey.api.data.TableInfo; +import org.labkey.api.data.dialect.SqlDialect; +import org.labkey.api.exp.ChangePropertyDescriptorException; +import org.labkey.api.exp.DomainDescriptor; +import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.PropertyType; +import org.labkey.api.exp.api.StorageProvisioner; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainKind; +import org.labkey.api.exp.property.DomainProperty; +import org.labkey.api.exp.property.IPropertyType; +import org.labkey.api.exp.property.IPropertyValidator; +import org.labkey.api.exp.property.Lookup; +import org.labkey.api.exp.property.PropertyService; +import org.labkey.api.exp.property.SystemProperty; +import org.labkey.api.gwt.client.DefaultScaleType; +import org.labkey.api.gwt.client.DefaultValueType; +import org.labkey.api.gwt.client.FacetingBehaviorType; +import org.labkey.api.query.QueryChangeListener; +import org.labkey.api.query.SchemaKey; +import org.labkey.api.security.User; +import org.labkey.api.util.StringExpressionFactory; +import org.labkey.api.util.TestContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static org.labkey.api.data.ColumnRenderPropertiesImpl.TEXT_CHOICE_CONCEPT_URI; + +public class DomainPropertyImpl implements DomainProperty +{ + private final DomainImpl _domain; + + PropertyDescriptor _pd; + PropertyDescriptor _pdOld; + boolean _deleted; + + private boolean _schemaChanged; + private boolean _schemaImport; + private List _validators; + private List _formats; + private String _defaultValue; + + public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd) + { + this(type, pd, null); + } + + public DomainPropertyImpl(DomainImpl type, PropertyDescriptor pd, List formats) + { + _domain = type; + _pd = pd.clone(); + _formats = formats; + } + + @Override + public int getPropertyId() + { + return _pd.getPropertyId(); + } + + @Override + public Container getContainer() + { + return _pd.getContainer(); + } + + @Override + public String getPropertyURI() + { + return _pd.getPropertyURI(); + } + + @Override + public String getName() + { + return _pd.getName(); + } + + @Override + public String getDescription() + { + return _pd.getDescription(); + } + + @Override + public String getFormat() + { + return _pd.getFormat(); + } + + @Override + public String getLabel() + { + return _pd.getLabel(); + } + + @Override + public String getConceptURI() + { + return _pd.getConceptURI(); + } + + @Override + public Domain getDomain() + { + return _domain; + } + + @Override + public IPropertyType getType() + { + return PropertyService.get().getType(getContainer(), _pd.getRangeURI()); + } + + @Override + public boolean isRequired() + { + return _pd.isRequired(); + } + + @Override + public boolean isHidden() + { + return _pd.isHidden(); + } + + @Override + public boolean isDeleted() + { + return _deleted; + } + + @Override + public boolean isShownInInsertView() + { + return _pd.isShownInInsertView(); + } + + @Override + public boolean isShownInDetailsView() + { + return _pd.isShownInDetailsView(); + } + + @Override + public boolean isShownInUpdateView() + { + return _pd.isShownInUpdateView(); + } + + @Override + public boolean isShownInLookupView() + { + return _pd.isShownInLookupView(); + } + + @Override + public boolean isMeasure() + { + return _pd.isMeasure(); + } + + @Override + public boolean isDimension() + { + return _pd.isDimension(); + } + + @Override + public boolean isRecommendedVariable() + { + return _pd.isRecommendedVariable(); + } + + @Override + public DefaultScaleType getDefaultScale() + { + return _pd.getDefaultScale(); + } + + @Override + public PHI getPHI() + { + return _pd.getPHI(); + } + + @Override + public String getRedactedText() { return _pd.getRedactedText(); } + + @Override + public boolean isExcludeFromShifting() + { + return _pd.isExcludeFromShifting(); + } + + @Override + public boolean isMvEnabled() + { + return _pd.isMvEnabled(); + } + + @Override + public boolean isMvEnabledForDrop() + { + if (null != _pdOld) + return _pdOld.isMvEnabled(); // if we need to drop/recreate we care about the old one + return _pd.isMvEnabled(); + } + + @Override + public void delete() + { + _deleted = true; + } + + @Override + public void setSchemaImport(boolean isSchemaImport) + { + // if this flag is set True then the column is dropped and recreated by its Domain if there is a type change + _schemaImport = isSchemaImport; + } + + @Override + public void setName(String name) + { + if (Strings.CS.equals(name, getName())) + return; + edit().setName(name); + } + + @Override + public void setDescription(String description) + { + if (Strings.CS.equals(description, getDescription())) + return; + edit().setDescription(description); + } + + @Override + public void setType(IPropertyType domain) + { + edit().setRangeURI(domain.getTypeURI()); + } + + @Override + public void setPropertyURI(String uri) + { + if (Strings.CS.equals(uri, getPropertyURI())) + return; + edit().setPropertyURI(uri); + } + + @Override + public void setRangeURI(String rangeURI) + { + if (Strings.CS.equals(rangeURI, getRangeURI())) + return; + editSchema().setRangeURI(rangeURI); + } + + @Override + public String getRangeURI() + { + return _pd.getRangeURI(); + } + + @Override + public void setFormat(String s) + { + if (Strings.CS.equals(s, getFormat())) + return; + edit().setFormat(s); + } + + @Override + public void setLabel(String caption) + { + if (Strings.CS.equals(caption, getLabel())) + return; + edit().setLabel(caption); + } + + @Override + public void setConceptURI(String conceptURI) + { + if (Strings.CS.equals(conceptURI, getConceptURI())) + return; + edit().setConceptURI(conceptURI); + } + + @Override + public void setRequired(boolean required) + { + if (required == isRequired()) + return; + edit().setRequired(required); + } + + @Override + public void setHidden(boolean hidden) + { + if (hidden == isHidden()) + return; + edit().setHidden(hidden); + } + + @Override + public void setShownInDetailsView(boolean shown) + { + if (shown == isShownInDetailsView()) + return; + edit().setShownInDetailsView(shown); + } + + @Override + public void setShownInInsertView(boolean shown) + { + if (shown == isShownInInsertView()) + return; + edit().setShownInInsertView(shown); + } + + @Override + public void setShownInUpdateView(boolean shown) + { + if (shown == isShownInUpdateView()) + return; + edit().setShownInUpdateView(shown); + } + + @Override + public void setShownInLookupView(boolean shown) + { + if (shown == isShownInLookupView()) + return; + edit().setShownInLookupView(shown); + } + + @Override + public void setMeasure(boolean isMeasure) + { + // UNDONE: isMeasure() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isMeasure == isMeasure()) + return; + edit().setMeasure(isMeasure); + } + + @Override + public void setDimension(boolean isDimension) + { + // UNDONE: isDimension() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isDimension == isDimension()) + return; + edit().setDimension(isDimension); + } + + @Override + public void setRecommendedVariable(boolean isRecommendedVariable) + { + if (!isEdited() && isRecommendedVariable == isRecommendedVariable()) + return; + edit().setRecommendedVariable(isRecommendedVariable); + } + + @Override + public void setDefaultScale(DefaultScaleType defaultScale) + { + if (!isEdited() && getDefaultScale() == defaultScale) + return; + + edit().setDefaultScale(defaultScale); + } + + @Override + public void setPhi(PHI phi) + { + if (!isEdited() && getPHI() == phi) + return; + edit().setPHI(phi); + } + + @Override + public void setRedactedText(String redactedText) + { + if (!isEdited() && ((getRedactedText() != null && getRedactedText().equals(redactedText)) + || (getRedactedText() == null && redactedText == null))) + return; + edit().setRedactedText(redactedText); + } + + @Override + public void setExcludeFromShifting(boolean isExcludeFromShifting) + { + // UNDONE: isExcludeFromShifting() has side-effect due to calling isNumeric()->getSqlTypeInt() which relies on rangeURI which might not be set yet. + if (!isEdited() && isExcludeFromShifting == isExcludeFromShifting()) + return; + edit().setExcludeFromShifting(isExcludeFromShifting); + } + + @Override + public void setMvEnabled(boolean mv) + { + if (mv == isMvEnabled()) + return; + edit().setMvEnabled(mv); + } + + @Override + public void setScale(int scale) + { + if (scale == getScale()) + return; + edit().setScale(scale); + } + + /** Need the string version of this method because it's called by reflection and must match by name */ + public void setImportAliases(String aliases) + { + if (Strings.CS.equals(aliases, getImportAliases())) + return; + edit().setImportAliases(aliases); + } + + /** Need the string version of this method because it's called by reflection and must match by name */ + public String getImportAliases() + { + return _pd.getImportAliases(); + } + + @Override + public void setImportAliasSet(Set aliases) + { + String current = getImportAliases(); + String newAliases = ColumnRenderPropertiesImpl.convertToString(aliases); + if (Strings.CS.equals(current, newAliases)) + return; + edit().setImportAliasesSet(aliases); + } + + @Override + public Set getImportAliasSet() + { + return _pd.getImportAliasSet(); + } + + @Override + public void setURL(String url) + { + if (Strings.CS.equals(getURL(), url)) + return; + + if (null == url) + edit().setURL(null); + else + edit().setURL(StringExpressionFactory.createURL(url)); + } + + @Override + public String getURL() + { + return _pd.getURL() == null ? null : _pd.getURL().toString(); + } + + @Override + public void setURLTarget(String urlTarget) + { + if (Strings.CS.equals(getURLTarget(), urlTarget)) + return; + edit().setURLTarget(urlTarget); + } + + @Override + public String getURLTarget() + { + return _pd.getURLTarget(); + } + + private boolean isEdited() + { + return null != _pdOld; + } + + private PropertyDescriptor editSchema() + { + PropertyDescriptor pd = edit(); + _schemaChanged = true; + _pd.clearPropertyType(); + return pd; + } + + public boolean isRecreateRequired() + { + return _schemaChanged && _schemaImport; + } + + public void markAsNew() + { + assert isRecreateRequired() && !isNew(); + _pd.setPropertyId(0); + } + + private PropertyDescriptor edit() + { + if (_pdOld == null) + { + _pdOld = _pd; + _pd = _pdOld.clone(); + } + return _pd; + } + + @Override + public PropertyType getPropertyType() + { + return _pd.getPropertyType(); + } + + @Override + public JdbcType getJdbcType() + { + return _pd.getPropertyType().getJdbcType(); + } + + @Override + public int getScale() + { + return _pd.getScale(); + } + + @Override + public String getInputType() + { + return _pd.getPropertyType().getInputType(); + } + + @Override + public DefaultValueType getDefaultValueTypeEnum() + { + return _pd.getDefaultValueTypeEnum(); + } + + @Override + public void setDefaultValueTypeEnum(DefaultValueType defaultValueType) + { + _pd.setDefaultValueTypeEnum(defaultValueType); + } + + public String getDefaultValueType() + { + return _pd.getDefaultValueType(); + } + + @Override + public void setDefaultValueType(String defaultValueTypeName) + { + if (getDefaultValueType() != null && getDefaultValueType().equals(defaultValueTypeName)) + return; + + if (getDefaultValueType() == null && defaultValueTypeName == null) + return; // if both are null, don't call edit(), with marks property as dirty + + edit().setDefaultValueType(defaultValueTypeName); + } + + @Override + public void setDefaultValue(String value) + { + _defaultValue = value; + } + + public String getDefaultValue() + { + return _defaultValue; + } + + @Override + public Lookup getLookup() + { + return _pd.getLookup(); + } + + @Override + public void setLookup(Lookup lookup) + { + Lookup current = getLookup(); + + if (current == lookup) + return; + + // current will return null if the schema or query is null so check + // for this case in the passed in lookup + if (current == null) + if (lookup.getQueryName() == null || lookup.getSchemaKey() == null) + return; + + if (current != null && current.equals(lookup)) + return; + + if (lookup == null) + { + edit().setLookupContainer(null); + edit().setLookupSchema(null); + edit().setLookupQuery(null); + return; + } + if (lookup.getContainer() == null) + { + edit().setLookupContainer(null); + } + else + { + edit().setLookupContainer(lookup.getContainer().getId()); + } + edit().setLookupQuery(lookup.getQueryName()); + edit().setLookupSchema(Objects.toString(lookup.getSchemaKey(),null)); + } + + @Override + public void setScannable(boolean scannable) + { + if (scannable != isScannable()) + edit().setScannable(scannable); + } + + @Override + public void setOldPropertyDescriptor(PropertyDescriptor oldPropertyDescriptor) + { + if (isEdited()) + return; + + _pdOld = oldPropertyDescriptor.clone(); + } + + @Override + public boolean isScannable() + { + return _pd.isScannable(); + } + + @Override + public void setPrincipalConceptCode(String code) + { + if (!Strings.CS.equals(code, getPrincipalConceptCode())) + edit().setPrincipalConceptCode(code); + } + + @Override + public String getPrincipalConceptCode() + { + return _pd.getPrincipalConceptCode(); + } + + @Override + public String getSourceOntology() + { + return _pd.getSourceOntology(); + } + + @Override + public void setSourceOntology(String sourceOntology) + { + if (!Strings.CS.equals(sourceOntology, getSourceOntology())) + edit().setSourceOntology(sourceOntology); + } + + @Override + public String getConceptSubtree() + { + return _pd.getConceptSubtree(); + } + + @Override + public void setConceptSubtree(String path) + { + if (!Strings.CS.equals(path, getConceptSubtree())) + edit().setConceptSubtree(path); + } + + @Override + public String getConceptImportColumn() + { + return _pd.getConceptImportColumn(); + } + + @Override + public void setConceptImportColumn(String conceptImportColumn) + { + if (!Strings.CS.equals(conceptImportColumn, getConceptImportColumn())) + edit().setConceptImportColumn(conceptImportColumn); + } + + @Override + public String getConceptLabelColumn() + { + return _pd.getConceptLabelColumn(); + } + + @Override + public void setConceptLabelColumn(String conceptLabelColumn) + { + if (!Strings.CS.equals(conceptLabelColumn, getConceptLabelColumn())) + edit().setConceptLabelColumn(conceptLabelColumn); + } + + @Override + public void setDerivationDataScope(String scope) + { + if (!Strings.CS.equals(scope, getDerivationDataScope())) + edit().setDerivationDataScope(scope); + } + + @Override + public String getDerivationDataScope() + { + return _pd.getDerivationDataScope(); + } + + @Override + public PropertyDescriptor getPropertyDescriptor() + { + return _pd; + } + + @Override + public List getConditionalFormats() + { + return ensureConditionalFormats(); + } + + public boolean isNew() + { + return _pd.getPropertyId() == 0; + } + + // Scenario to swap property descriptors on study upload to or from a system property, instead of updating the + // current property descriptor. Avoids overwriting a system property. + public boolean isSystemPropertySwap() + { + if (_pd.getPropertyId() == 0 && _pd.getPropertyURI() != null && _pdOld != null && _pdOld.getPropertyURI() != null + && !_pd.getPropertyURI().equals(_pdOld.getPropertyURI())) + { + return SystemProperty.getProperties().stream().anyMatch(sp -> + sp.getPropertyURI().equals(_pd.getPropertyURI()) || sp.getPropertyURI().equals(_pdOld.getPropertyURI())); + } + + return false; + } + + public boolean isDirty() + { + if (_pdOld != null) return true; + + for (PropertyValidatorImpl v : ensureValidators()) + { + if (v.isDirty() || v.isNew()) + return true; + } + return false; + } + + public void delete(User user) + { + DomainPropertyManager.get().removeValidatorsForPropertyDescriptor(getContainer(), getPropertyId()); + DomainPropertyManager.get().deleteConditionalFormats(getPropertyId()); + + DomainKind kind = getDomain().getDomainKind(); + if (null != kind) + kind.deletePropertyDescriptor(getDomain(), user, _pd); + OntologyManager.removePropertyDescriptorFromDomain(this); + } + + public void save(User user, DomainDescriptor dd, int sortOrder) throws ChangePropertyDescriptorException + { + if (isSystemPropertySwap()) + { + _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); + OntologyManager.removePropertyDescriptorFromDomain(new DomainPropertyImpl((DomainImpl) getDomain(), _pdOld)); + } + else if (isNew()) + { + _pd = OntologyManager.insertOrUpdatePropertyDescriptor(_pd, dd, sortOrder); + } + else if (_pdOld != null) + { + PropertyType oldType = _pdOld.getPropertyType(); + PropertyType newType = _pd.getPropertyType(); + boolean changedType = false; + if (oldType.getJdbcType() != newType.getJdbcType()) + { + if (newType.getJdbcType().isText() || + (oldType.getJdbcType().isInteger() && newType.getJdbcType().isNumeric())) + { + changedType = true; + if (newType.getJdbcType().isText()) + { + // Remove any previously set formatting string as it won't apply to a text field + _pd.setFormat(null); + } + } + else if (newType.getJdbcType().isDateOrTime() && oldType.getJdbcType().isDateOrTime()) + { + changedType = true; + _pd.setFormat(null); + } + else if (newType == PropertyType.MULTI_CHOICE || oldType == PropertyType.MULTI_CHOICE) + { + changedType = true; + _pd.setFormat(null); + } + else + { + throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.getJdbcType() + " to " + newType.getJdbcType() + "."); + } + } + + // Issue 44711: Prevent attachment and file field types from being converted to a different type + if (PropertyType.FILE_LINK.getInputType().equalsIgnoreCase(oldType.getInputType()) && oldType != newType) + throw new ChangePropertyDescriptorException("Cannot convert an instance of " + oldType.name() + " to " + newType.name() + "."); + + // GitHub Issue 951: Multi-line values converted to text choices lose multi-line editability + if (oldType == PropertyType.MULTI_LINE && + (PropertyType.MULTI_CHOICE == newType ||TEXT_CHOICE_CONCEPT_URI.equals(_pd.getConceptURI()))) + { + throw new ChangePropertyDescriptorException("Cannot convert a multiline text field to a text choice field."); + } + + OntologyManager.validatePropertyDescriptor(_pd); + Table.update(user, OntologyManager.getTinfoPropertyDescriptor(), _pd, _pdOld.getPropertyId()); + OntologyManager.ensurePropertyDomain(_pd, dd, sortOrder); + + boolean hasProvisioner = null != getDomain().getDomainKind() && null != getDomain().getDomainKind().getStorageSchemaName() && dd.getStorageTableName() != null; + SqlDialect dialect = OntologyManager.getExpSchema().getSqlDialect(); + + if (hasProvisioner) + { + boolean mvAdded = !_pdOld.isMvEnabled() && _pd.isMvEnabled(); + boolean mvDropped = _pdOld.isMvEnabled() && !_pd.isMvEnabled(); + boolean propRenamed = !_pdOld.getName().equals(_pd.getName()); + boolean propResized = _pd.isStringType() && _pdOld.getScale() != _pd.getScale(); + + // Drop first, so rename doesn't have to worry about it + if (mvDropped) + ((StorageProvisionerImpl)StorageProvisioner.get()).dropMvIndicator(this, _pdOld); + + if (propRenamed) + StorageProvisionerImpl.get().renameProperty(this.getDomain(), this, _pdOld, mvDropped); + + if (changedType) + { + var domainKind = _domain.getDomainKind(); + if (domainKind == null) + throw new ChangePropertyDescriptorException("Cannot change property type for domain, unknown domain kind."); + + StorageProvisionerImpl.get().changePropertyType(this.getDomain(), this); + if (_pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) + { + updateBooleanValue( + new SQLFragment().appendIdentifier(domainKind.getStorageSchemaName()).append(".").appendIdentifier(_domain.getStorageTableName()), + _pd.getLegalSelectName(dialect), _pdOld.getFormat(), null); // GitHub Issue #647 + } + + TableInfo table = domainKind.getTableInfo(user, getContainer(), _domain, ContainerFilter.getUnsafeEverythingFilter()); + if (table != null && _pdOld.getPropertyType() != null && table.getSchema().getSqlDialect().isPostgreSQL()) + QueryChangeListener.QueryPropertyChange.handleColumnTypeChange(_pdOld, _pd, SchemaKey.fromString(table.getUserSchema().getSchemaName()), table.getName(), user, getContainer()); + } + else if (propResized) + StorageProvisionerImpl.get().resizeProperty(this.getDomain(), this, _pdOld.getScale()); + + if (mvAdded) + StorageProvisionerImpl.get().addMvIndicator(this); + } + else if (changedType) + { + if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isText()) + { + new SqlExecutor(OntologyManager.getExpSchema()).execute( + new SQLFragment("UPDATE "). + append(OntologyManager.getTinfoObjectProperty()). + append(" SET StringValue = DateTimeValue, DateTimeValue = NULL WHERE PropertyId = ?"). + add(_pdOld.getPropertyId())); + } + else if (!oldType.getJdbcType().isText() && newType.getJdbcType().isText()) + { + new SqlExecutor(OntologyManager.getExpSchema()).execute( + new SQLFragment("UPDATE "). + append(OntologyManager.getTinfoObjectProperty()). + append(" SET StringValue = FloatValue, FloatValue = NULL WHERE PropertyId = ?"). + add(_pdOld.getPropertyId())); + } + else if (oldType.getJdbcType().isDateOrTime() && newType.getJdbcType().isDateOrTime()) + { + String sqlTypeName = dialect.getSqlTypeName(newType.getJdbcType()); + String update = String.format("CAST(DateTimeValue AS %s)", sqlTypeName); + if (newType.getJdbcType() == JdbcType.TIME) + update = dialect.getDateTimeToTimeCast("DateTimeValue"); + SQLFragment sqlFragment = new SQLFragment("UPDATE ") + .append(OntologyManager.getTinfoObjectProperty()) + .append(" SET DateTimeValue = ") + .append(update) + .append(" WHERE PropertyId = ?") + .add(_pdOld.getPropertyId()); + new SqlExecutor(OntologyManager.getExpSchema()).execute(sqlFragment); + } + else //noinspection StatementWithEmptyBody + if (oldType.getJdbcType().isInteger() && newType.getJdbcType().isReal()) + { + // Since exp.ObjectProperty stores these types in the same column, there's nothing for us to do + } + else + { + throw new ChangePropertyDescriptorException("Cannot convert from " + oldType.getJdbcType() + " to " + newType.getJdbcType() + " for non-provisioned table"); + } + } + + if (changedType && _pdOld.getJdbcType() == JdbcType.BOOLEAN && _pd.getJdbcType().isText()) + { + updateBooleanValue(OntologyManager.getTinfoObjectProperty().getSQLName(), dialect.makeDatabaseIdentifier("StringValue"), _pdOld.getFormat(), new SQLFragment("PropertyId = ?", _pdOld.getPropertyId())); + } + } + else + { + OntologyManager.ensurePropertyDomain(_pd, _domain._dd, sortOrder); + } + + _pdOld = null; + _schemaChanged = false; + _schemaImport = false; + + for (PropertyValidatorImpl validator : ensureValidators()) + { + if (validator.isDeleted()) + DomainPropertyManager.get().removePropertyValidator(this, validator); + else + DomainPropertyManager.get().savePropertyValidator(user, this, validator); + } + + DomainPropertyManager.get().saveConditionalFormats(user, getPropertyDescriptor(), ensureConditionalFormats()); + } + + /** + * Format values in columns that were just converted from booleans to strings with the DB's default type conversion. + * Postgres will now have 'true' and 'false', and SQLServer will have '0' and '1'. Use the format string to use the + * preferred format, and standardize on 'true' and 'false' in the absence of an explicitly configured format. + */ + private void updateBooleanValue(SQLFragment schemaTable, DatabaseIdentifier column, String formatString, @Nullable SQLFragment whereClause) + { + BooleanFormat f = BooleanFormat.getInstance(formatString); + String trueValue = StringUtils.trimToNull(f.format(true)); + String falseValue = StringUtils.trimToNull(f.format(false)); + String nullValue = StringUtils.trimToNull(f.format(null)); + SQLFragment sql = new SQLFragment("UPDATE ").append(schemaTable).append(" SET "). + appendIdentifier(column).append(" = CASE WHEN "). + appendIdentifier(column).append(" IN ('1', 'true') THEN ? WHEN "). + appendIdentifier(column).append(" IN ('0', 'false') THEN ? ELSE ? END"); + sql.add(trueValue); + sql.add(falseValue); + sql.add(nullValue); + if (whereClause != null) + { + sql.append(" WHERE "); + sql.append(whereClause); + } + new SqlExecutor(OntologyManager.getExpSchema()).execute(sql); + } + + @Override + @NotNull + public List getValidators() + { + return Collections.unmodifiableList(ensureValidators()); + } + + @Override + public void addValidator(IPropertyValidator validator) + { + if (validator != null) + { + if (0 != validator.getPropertyId() && getPropertyId() != validator.getPropertyId()) + throw new IllegalStateException(); + + // Ensure validator is a valid kind (ex. urn:lsid:labkey.com:PropertyValidator:length is no longer valid) + if ( null != PropertyService.get().getValidatorKind(validator.getTypeURI()) ) + { + PropertyValidator impl = new PropertyValidator(); + impl.copy(validator); + impl.setPropertyId(getPropertyId()); + ensureValidators().add(new PropertyValidatorImpl(impl)); + } + } + } + + @Override + public void removeValidator(IPropertyValidator validator) + { + int idx = ensureValidators().indexOf(validator); + if (idx != -1) + { + PropertyValidatorImpl impl = ensureValidators().get(idx); + impl.delete(); + } + } + + @Override + public void removeValidator(long validatorId) + { + if (validatorId == 0) return; + + for (PropertyValidatorImpl imp : ensureValidators()) + { + if (imp.getRowId() == validatorId) + { + imp.delete(); + break; + } + } + } + + @Override + public void copyFrom(DomainProperty propSrc, Container targetContainer) + { + setDescription(propSrc.getDescription()); + setFormat(propSrc.getFormat()); + setLabel(propSrc.getLabel()); + setName(propSrc.getName()); + setDescription(propSrc.getDescription()); + setConceptURI(propSrc.getConceptURI()); + setType(propSrc.getType()); + setDimension(propSrc.isDimension()); + setMeasure(propSrc.isMeasure()); + setRecommendedVariable(propSrc.isRecommendedVariable()); + setDefaultScale(propSrc.getDefaultScale()); + setRequired(propSrc.isRequired()); + setExcludeFromShifting(propSrc.isExcludeFromShifting()); + setFacetingBehavior(propSrc.getFacetingBehavior()); + setImportAliasSet(propSrc.getImportAliasSet()); + setPhi(propSrc.getPHI()); + setURL(propSrc.getURL()); + setURLTarget(propSrc.getURLTarget()); + setHidden(propSrc.isHidden()); + setShownInDetailsView(propSrc.isShownInDetailsView()); + setShownInInsertView(propSrc.isShownInInsertView()); + setShownInUpdateView(propSrc.isShownInUpdateView()); + setShownInLookupView(propSrc.isShownInLookupView()); + setMvEnabled(propSrc.isMvEnabled()); + setDefaultValueTypeEnum(propSrc.getDefaultValueTypeEnum()); + setScale(propSrc.getScale()); + setScannable(propSrc.isScannable()); + + setPrincipalConceptCode(propSrc.getPrincipalConceptCode()); + setSourceOntology(propSrc.getSourceOntology()); + setConceptSubtree(propSrc.getConceptSubtree()); + setConceptImportColumn(propSrc.getConceptImportColumn()); + setConceptLabelColumn(propSrc.getConceptLabelColumn()); + setDerivationDataScope(propSrc.getDerivationDataScope()); + + // check to see if we're moving a lookup column to another container: + Lookup lookup = propSrc.getLookup(); + if (lookup != null && !getContainer().equals(targetContainer)) + { + // we need to update the lookup properties if the lookup container is either the source or the destination container + if (lookup.getContainer() == null) + lookup.setContainer(propSrc.getContainer()); + else if (lookup.getContainer().equals(targetContainer)) + lookup.setContainer(null); + } + setLookup(lookup); + } + + @Override + public void setConditionalFormats(List formats) + { + String newVal = ConditionalFormat.toStringVal(formats); + String oldVal = ConditionalFormat.toStringVal(getConditionalFormats()); + + if (!Objects.equals(newVal, oldVal)) + edit(); + + _formats = formats; + } + + private List ensureValidators() + { + if (_validators == null) + { + _validators = new ArrayList<>(); + for (PropertyValidator validator : DomainPropertyManager.get().getValidators(this)) + { + _validators.add(new PropertyValidatorImpl(validator)); + } + } + return _validators; + } + + private List ensureConditionalFormats() + { + if (_formats == null) + { + _formats = new ArrayList<>(); + _formats.addAll(DomainPropertyManager.get().getConditionalFormats(this)); + } + return _formats; + } + + public PropertyDescriptor getOldProperty() + { + return _pdOld; + } + + @Override + public FacetingBehaviorType getFacetingBehavior() + { + return _pd.getFacetingBehaviorType(); + } + + @Override + public void setFacetingBehavior(FacetingBehaviorType type) + { + if (getFacetingBehavior() == type) + return; + + edit().setFacetingBehaviorType(type); + } + + @Override + public int hashCode() + { + return _pd.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + if (!(obj instanceof DomainPropertyImpl)) + return false; + // once a domain property has been edited, it no longer equals any other domain property: + if (_pdOld != null || ((DomainPropertyImpl) obj)._pdOld != null) + return false; + return (_pd.equals(((DomainPropertyImpl) obj)._pd)); + } + + @Override + public String toString() + { + return super.toString() + _pd.getPropertyURI(); + } + + public Map getAuditRecordMap(@Nullable String validatorStr, @Nullable String conditionalFormatStr) + { + Map map = new LinkedHashMap<>(); + if (!StringUtils.isEmpty(getName())) + map.put("Name", getName()); + if (!StringUtils.isEmpty(getLabel())) + map.put("Label", getLabel()); + if (null != getPropertyType()) + map.put("Type", getPropertyType().getXarName()); + if (getPropertyType().getJdbcType().isText()) + map.put("Scale", getScale()); + if (!StringUtils.isEmpty(getDescription())) + map.put("Description", getDescription()); + if (!StringUtils.isEmpty(getFormat())) + map.put("Format", getFormat()); + if (!StringUtils.isEmpty(getURL())) + map.put("URL", getURL()); + if (!StringUtils.isEmpty(getURLTarget())) + map.put("URLTarget", getURLTarget()); + if (getPHI() != null) + map.put("PHI", getPHI().getLabel()); + if (getDefaultScale() != null) + map.put("DefaultScale", getDefaultScale().getLabel()); + map.put("Required", isRequired()); + map.put("Hidden", isHidden()); + map.put("MvEnabled", isMvEnabled()); + map.put("Measure", isMeasure()); + map.put("Dimension", isDimension()); + map.put("ShownInInsert", isShownInInsertView()); + map.put("ShownInDetails", isShownInDetailsView()); + map.put("ShownInUpdate", isShownInUpdateView()); + map.put("ShownInLookupView", isShownInLookupView()); + map.put("RecommendedVariable", isRecommendedVariable()); + map.put("ExcludedFromShifting", isExcludeFromShifting()); + map.put("Scannable", isScannable()); + if (!StringUtils.isEmpty(getDerivationDataScope())) + map.put("DerivationDataScope", getDerivationDataScope()); + String importAliasStr = StringUtils.join(getImportAliasSet(), ","); + if (!StringUtils.isEmpty(importAliasStr)) + map.put("ImportAliases", importAliasStr); + if (getDefaultValueTypeEnum() != null) + map.put("DefaultValueType", getDefaultValueTypeEnum().getLabel()); + if (getLookup() != null) + map.put("Lookup", getLookup().toJSONString()); + + if (!StringUtils.isEmpty(validatorStr)) + map.put("Validator", validatorStr); + if (!StringUtils.isEmpty(conditionalFormatStr)) + map.put("ConditionalFormat", conditionalFormatStr); + + return map; + } + + public static class TestCase extends Assert + { + private PropertyDescriptor _pd; + private DomainPropertyImpl _dp; + + @Test + public void testUpdateDomainPropertyFromDescriptor() + { + Container c = ContainerManager.ensureContainer("/_DomainPropertyImplTest", TestContext.get().getUser()); + String domainURI = new Lsid("Junit", "DD", "Domain1").toString(); + Domain d = PropertyService.get().createDomain(c, domainURI, "Domain1"); + + resetProperties(d, domainURI, c); + + // verify no change + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertFalse(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + + // change a property + _pd.setPHI(PHI.Restricted); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + assertTrue(_dp.getPHI() == _pd.getPHI()); + + // Issue #18738 change the schema outside of a schema reload and verify that the column + // change the schema but don't mark the property as "Schema Import" + // this will allow whatever type changes the UI allows (text -> multiline, for example) + resetProperties(d, domainURI, c); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertTrue(_dp._schemaChanged); + assertFalse(_dp.isRecreateRequired()); + assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); + + // setting schema import to true will enable the _schemaChanged flag to toggle + // so it should be set true here + resetProperties(d, domainURI, c); + _dp.setSchemaImport(true); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#double"); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty()); + assertTrue(_dp._schemaChanged); + assertTrue(_dp.isRecreateRequired()); + assertTrue(Strings.CS.equals(_dp.getRangeURI(), _pd.getRangeURI())); + + // verify no change when setting value to the same value as it was + resetProperties(d, domainURI, c); + _pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); + _pd.setPHI(PHI.NotPHI); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertFalse(_dp.isDirty()); + assertFalse(_dp._schemaChanged); + assertFalse(_dp.isRecreateRequired()); + + // verify Lookup is set to null with null schema + resetProperties(d, domainURI, c); + verifyLookup(null, "lkSchema", null, true); + + // verify Lookup is set to null with null query + resetProperties(d, domainURI, c); + verifyLookup(null, null, "lkQuery",true); + + // verify Lookup is set to null with invalid container + resetProperties(d, domainURI, c); + verifyLookup("bogus", null, "lkQuery",true); + + // verify Lookup is set with valid schema and query + resetProperties(d, domainURI, c); + verifyLookup(null, "lkSchema", "lkQuery",true); + + // verify Lookup is set with valid container, schema and query + resetProperties(d, domainURI, c); + verifyLookup(c.getId(), "lkSchema1", "lkQuery2",true); + + // no cleanup as we never persisted anything + } + + private void verifyLookup(String containerId, String schema, String query, Boolean expectedDirty) + { + _pd.setLookupContainer(containerId); + _pd.setLookupQuery(query); + _pd.setLookupSchema(schema); + OntologyManager.updateDomainPropertyFromDescriptor(_dp, _pd); + assertTrue(_dp.isDirty() == expectedDirty); + assertFalse(_dp._schemaChanged); + + // verify the lookup object returned + Lookup l = _dp.getLookup(); + + if (l == null) + { + // lookup can be null if we specified a containerId that is invalid or + // we specified a valid containerId (including null) but schema or query is null + if (containerId != null && null == ContainerManager.getForId(containerId)) + assertTrue(true); + else if (query == null || schema == null) + assertTrue(true); + else + assertTrue(false); + } + else + { + if (containerId != null) + assertTrue(Strings.CS.equals(l.getContainer().getId(), _pd.getLookupContainer())); + + assertTrue(Strings.CS.equals(l.getQueryName(), _pd.getLookupQuery())); + assertTrue(Strings.CS.equals(l.getSchemaKey().toString(), _pd.getLookupSchema())); + } + } + + private void resetProperties(Domain d, String domainUri, Container c) + { + _pd = getPropertyDescriptor(c, domainUri); + _dp = (DomainPropertyImpl) d.addProperty(); + _pd.copyTo(_dp.getPropertyDescriptor()); + } + + + private PropertyDescriptor getPropertyDescriptor(Container c, String domainURI) + { + PropertyDescriptor pd = new PropertyDescriptor(); + pd.setPropertyURI(domainURI + ":column"); + pd.setName("column"); + pd.setLabel("label"); + pd.setConceptURI(null); + pd.setRangeURI("http://www.w3.org/2001/XMLSchema#int"); + pd.setContainer(c); + pd.setDescription("description"); + pd.setURL(StringExpressionFactory.createURL((String)null)); + pd.setURLTarget(null); + pd.setImportAliases(null); + pd.setRequired(false); + pd.setHidden(false); + pd.setShownInInsertView(true); + pd.setShownInUpdateView(true); + pd.setShownInDetailsView(true); + pd.setDimension(false); + pd.setMeasure(true); + pd.setRecommendedVariable(false); + pd.setDefaultScale(DefaultScaleType.LINEAR); + pd.setFormat(null); + pd.setMvEnabled(false); + pd.setLookupContainer(c.getId()); + pd.setLookupSchema("lkSchema"); + pd.setLookupQuery("lkQuery"); + pd.setFacetingBehaviorType(FacetingBehaviorType.AUTOMATIC); + pd.setPHI(PHI.NotPHI); + pd.setExcludeFromShifting(false); + return pd; + } + } + + +} From 40ec79715e01ffe23ea6b1f6217a47aeed2c88be Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 1 Apr 2026 06:51:43 -0700 Subject: [PATCH 3/4] GitHub Issue 966: Grid filter UI parses URL parameter incorrectly for array data --- core/package-lock.json | 16 ++++++++-------- core/package.json | 2 +- experiment/package-lock.json | 16 ++++++++-------- experiment/package.json | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index b73c7089e77..33e8607512a 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.26.4", + "@labkey/components": "7.26.5-fb-mvtcBatch3.1", "@labkey/themes": "1.8.0" }, "devDependencies": { @@ -3716,9 +3716,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.51.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.0.tgz", - "integrity": "sha512-pyYXCNbFF3HZQ8KVapcwBl/7cHlVDz0ysPlCRBUQ9czX6s2yLwiho0OWkBd9MpbGVkqGFihbTUsUAU+Y5rz5Pw==", + "version": "1.51.1-mvtcBatch3.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1-mvtcBatch3.1.tgz", + "integrity": "sha512-HB/2tcDrlYiIxKewtqoWsHkFOBdXdndbabq8GkIJ8yzv0z08PNfzT50r34QTCHPNQM//0Sd4WIymYYSCEfOf4g==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -3759,13 +3759,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.26.4", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.26.4.tgz", - "integrity": "sha512-pJ/P/qEiOdV2QYtLC+aDHcxUlroo5ENkYToYqb83+N4ksgmRYmv4oaTi0sIvXH0xSpRn3b31qys7gcHbL0yIbA==", + "version": "7.26.5-fb-mvtcBatch3.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.26.5-fb-mvtcBatch3.1.tgz", + "integrity": "sha512-L5TPDzeL5heHUYZ9rr3rW8ZasjZ8tRFvLZMhmCqrRbQRIVQZesioQ1s2BW5I5Z5huXKxKKAVzNyuH3ceQjK7+w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.51.0", + "@labkey/api": "1.51.1-mvtcBatch3.1", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.2", diff --git a/core/package.json b/core/package.json index f36db022851..3e0150f45e5 100644 --- a/core/package.json +++ b/core/package.json @@ -53,7 +53,7 @@ } }, "dependencies": { - "@labkey/components": "7.26.4", + "@labkey/components": "7.26.5-fb-mvtcBatch3.1", "@labkey/themes": "1.8.0" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 06fb01a56d0..95a177e78b3 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.26.4" + "@labkey/components": "7.26.5-fb-mvtcBatch3.1" }, "devDependencies": { "@labkey/build": "9.1.0", @@ -3566,9 +3566,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.51.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.0.tgz", - "integrity": "sha512-pyYXCNbFF3HZQ8KVapcwBl/7cHlVDz0ysPlCRBUQ9czX6s2yLwiho0OWkBd9MpbGVkqGFihbTUsUAU+Y5rz5Pw==", + "version": "1.51.1-mvtcBatch3.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.51.1-mvtcBatch3.1.tgz", + "integrity": "sha512-HB/2tcDrlYiIxKewtqoWsHkFOBdXdndbabq8GkIJ8yzv0z08PNfzT50r34QTCHPNQM//0Sd4WIymYYSCEfOf4g==", "license": "Apache-2.0" }, "node_modules/@labkey/build": { @@ -3609,13 +3609,13 @@ } }, "node_modules/@labkey/components": { - "version": "7.26.4", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.26.4.tgz", - "integrity": "sha512-pJ/P/qEiOdV2QYtLC+aDHcxUlroo5ENkYToYqb83+N4ksgmRYmv4oaTi0sIvXH0xSpRn3b31qys7gcHbL0yIbA==", + "version": "7.26.5-fb-mvtcBatch3.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.26.5-fb-mvtcBatch3.1.tgz", + "integrity": "sha512-L5TPDzeL5heHUYZ9rr3rW8ZasjZ8tRFvLZMhmCqrRbQRIVQZesioQ1s2BW5I5Z5huXKxKKAVzNyuH3ceQjK7+w==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.51.0", + "@labkey/api": "1.51.1-mvtcBatch3.1", "@testing-library/dom": "~10.4.1", "@testing-library/jest-dom": "~6.9.1", "@testing-library/react": "~16.3.2", diff --git a/experiment/package.json b/experiment/package.json index d6898a03485..18c1327df1e 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -13,7 +13,7 @@ "test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js" }, "dependencies": { - "@labkey/components": "7.26.4" + "@labkey/components": "7.26.5-fb-mvtcBatch3.1" }, "devDependencies": { "@labkey/build": "9.1.0", From 5c93a621715465520b3f3cb8910efc02b8dcccc9 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 1 Apr 2026 15:45:39 -0700 Subject: [PATCH 4/4] api test for GitHub Issue 951 --- .../test/integration/DataClassCrud.ispec.ts | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/experiment/src/client/test/integration/DataClassCrud.ispec.ts b/experiment/src/client/test/integration/DataClassCrud.ispec.ts index dae73a5380c..cdb326b2db2 100644 --- a/experiment/src/client/test/integration/DataClassCrud.ispec.ts +++ b/experiment/src/client/test/integration/DataClassCrud.ispec.ts @@ -511,10 +511,14 @@ describe('Multi Value Text Choice', () => { { ...MVTC_FIELD_PROP, name: fieldName + }, + { + name: 'MultiLineField', + rangeURI:"http://www.w3.org/2001/XMLSchema#multiLine", } ]; - let domainId = -1, domainURI = '', propertyId, propertyURI; + let domainId = -1, domainURI = '', propertyId, propertyURI, mlPropertyId, mlPropertyURI; const createPayload = { kind: 'DataClass', domainDesign: { name: dataType, fields }, @@ -541,6 +545,9 @@ describe('Multi Value Text Choice', () => { const field = domain.fields[0]; propertyId = field.propertyId; propertyURI = field.propertyURI; + const mlField = domain.fields[1]; + mlPropertyId = mlField.propertyId; + mlPropertyURI = mlField.propertyURI; return true; }); @@ -786,6 +793,12 @@ describe('Multi Value Text Choice', () => { name: fieldName, propertyId, propertyURI + }, + { + name: 'MultiLineField', + rangeURI:"http://www.w3.org/2001/XMLSchema#multiLine", + propertyId: mlPropertyId, + propertyURI: mlPropertyURI } ], domainId, @@ -801,6 +814,62 @@ describe('Multi Value Text Choice', () => { result = await getDataClassDataByName(dataNameImported[0], dataType, '*', topFolderOptions, editorUserOptions); expect(caseInsensitive(result, fieldName)).toEqual('Abnormal, Plasma'); // convert from ['Abnormal', 'Plasma'] to 'Abnormal, Plasma' + // GitHub Issue 951: Multi-line values converted to text choices lose multi-line editability + // verify cannot convert MultiLine field to MultiValue Text Choice + updatePayload = { + domainId, + domainDesign: { + name: dataType, + fields: [ + { + name: fieldName, + propertyId, + propertyURI + }, + { + ...MVTC_FIELD_PROP, + name: 'MultiLineField', + propertyId: mlPropertyId, + propertyURI: mlPropertyURI + } + ], + domainId, + domainURI + }, + options: { + rowId: dataClassRowId, + name: dataType, + nameExpression: 'S-${' + fieldNameInExpression + '}' + } + }; + failedUpdate = await server.post('property', 'saveDomain', updatePayload, {...topFolderOptions, ...adminOptions}); + expect(failedUpdate?.['body']?.['exception']).toContain('Cannot convert a multiline text field to a text choice field.'); + + // verify cannot convert MultiLine field to Text Choice field + updatePayload = { + domainId, + domainDesign: { + name: dataType, + fields: [ + { + ...TC_FIELD_PROP, + name: 'MultiLineField', + propertyId: mlPropertyId, + propertyURI: mlPropertyURI + } + ], + domainId, + domainURI + }, + options: { + rowId: dataClassRowId, + name: dataType, + nameExpression: 'S-${genId}' + } + }; + failedUpdate = await server.post('property', 'saveDomain', updatePayload, {...topFolderOptions, ...adminOptions}); + expect(failedUpdate?.['body']?.['exception']).toContain('Cannot convert a multiline text field to a text choice field.'); + }); });