diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java index a866ef0f3f5..8200dd1bcc9 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java +++ b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTConditionalFormat.java @@ -16,27 +16,27 @@ package org.labkey.api.gwt.client.model; import com.google.gwt.user.client.rpc.IsSerializable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import java.io.Serializable; -/** - * User: jeckels - * Date: Aug 23, 2010 - */ +@Getter +@Setter +@EqualsAndHashCode public class GWTConditionalFormat implements Serializable, IsSerializable { public static final String COLOR_REGEX = "[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]"; - public static final String DATA_REGION_NAME = "format"; public static final String COLUMN_NAME = "column"; - private boolean _bold = false; - private boolean _italic = false; - private boolean _strikethrough = false; - private String _textColor = null; - private String _backgroundColor = null; - - private String _filter; + private boolean bold = false; + private boolean italic = false; + private boolean strikethrough = false; + private String textColor = null; + private String backgroundColor = null; + private String filter; public GWTConditionalFormat() {} @@ -47,98 +47,6 @@ public GWTConditionalFormat(GWTConditionalFormat f) setStrikethrough(f.isStrikethrough()); setTextColor(f.getTextColor()); setBackgroundColor(f.getBackgroundColor()); - setFilter(f.getFilter()); } - - public boolean isBold() - { - return _bold; - } - - public void setBold(boolean bold) - { - _bold = bold; - } - - public boolean isItalic() - { - return _italic; - } - - public void setItalic(boolean italic) - { - _italic = italic; - } - - public boolean isStrikethrough() - { - return _strikethrough; - } - - public void setStrikethrough(boolean strikethrough) - { - _strikethrough = strikethrough; - } - - public String getTextColor() - { - return _textColor; - } - - public void setTextColor(String textColor) - { - _textColor = textColor; - } - - public String getBackgroundColor() - { - return _backgroundColor; - } - - public void setBackgroundColor(String backgroundColor) - { - _backgroundColor = backgroundColor; - } - - public String getFilter() - { - return _filter; - } - - public void setFilter(String filter) - { - _filter = filter; - } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (!(o instanceof GWTConditionalFormat)) return false; - - GWTConditionalFormat that = (GWTConditionalFormat) o; - - if (_bold != that._bold) return false; - if (_italic != that._italic) return false; - if (_strikethrough != that._strikethrough) return false; - if (_backgroundColor != null ? !_backgroundColor.equals(that._backgroundColor) : that._backgroundColor != null) - return false; - if (_filter != null ? !_filter.equals(that._filter) : that._filter != null) return false; - if (_textColor != null ? !_textColor.equals(that._textColor) : that._textColor != null) return false; - - return true; - } - - @Override - public int hashCode() - { - int result = (_bold ? 1 : 0); - result = 31 * result + (_italic ? 1 : 0); - result = 31 * result + (_strikethrough ? 1 : 0); - result = 31 * result + (_textColor != null ? _textColor.hashCode() : 0); - result = 31 * result + (_backgroundColor != null ? _backgroundColor.hashCode() : 0); - result = 31 * result + (_filter != null ? _filter.hashCode() : 0); - return result; - } } diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java index 903afee21d5..d802cdfc2c6 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java +++ b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTDomain.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.gwt.user.client.rpc.IsSerializable; +import lombok.Getter; +import lombok.Setter; import org.labkey.api.gwt.client.DefaultValueType; import org.labkey.api.gwt.client.util.PropertyUtil; @@ -27,58 +29,47 @@ import java.util.List; import java.util.Set; -/** - * User: matthewb - * Date: Apr 24, 2007 - * Time: 1:44:49 PM - */ public class GWTDomain implements IsSerializable { private String _ts; - private int domainId; - private String name; - private String domainURI; - private String domainKindName; - private String description; - private String container; - private boolean allowFileLinkProperties; - private boolean allowAttachmentProperties; - private boolean allowFlagProperties; - private boolean allowTextChoiceProperties; - private boolean allowSampleSubjectProperties; - private boolean allowTimepointProperties; - private boolean allowUniqueConstraintProperties; - private boolean allowCalculatedFields; - private boolean showDefaultValueSettings; + @Getter @Setter private int domainId; + @Getter @Setter private String name; + @Getter @Setter private String domainURI; + @Getter @Setter private String domainKindName; + @Getter @Setter private String description; + @Getter @Setter private String container; + @Getter @Setter private boolean allowFileLinkProperties; + @Getter @Setter private boolean allowAttachmentProperties; + @Getter @Setter private boolean allowFlagProperties; + @Getter @Setter private boolean allowTextChoiceProperties; + @Getter @Setter private boolean allowSampleSubjectProperties; + @Getter @Setter private boolean allowTimepointProperties; + @Getter @Setter private boolean allowUniqueConstraintProperties; + @Getter @Setter private boolean allowCalculatedFields; + @Getter @Setter private boolean showDefaultValueSettings; private DefaultValueType defaultDefaultValueType = null; private DefaultValueType[] defaultValueOptions = new DefaultValueType[0]; - private List fields = new ArrayList(); + private List fields = new ArrayList<>(); private List standardFields = null; private List calculatedFields = null; - private List indices = new ArrayList(); + @Getter @Setter private List indices = new ArrayList<>(); private String defaultValuesURL = null; - - private Set mandatoryPropertyDescriptorNames = new HashSet(); - - private Set reservedFieldNames = new HashSet(); + private Set mandatoryPropertyDescriptorNames = new HashSet<>(); + private Set reservedFieldNames = new HashSet<>(); private Set reservedFieldNamePrefixes = new HashSet<>(); - private Set phiNotAllowedFieldNames = new HashSet(); - - private Set excludeFromExportFieldNames = new HashSet(); - private boolean provisioned = false; - - private List disabledSystemFields; + private Set phiNotAllowedFieldNames = new HashSet<>(); + private Set excludeFromExportFieldNames = new HashSet<>(); + @Getter @Setter private boolean provisioned = false; + @Getter @Setter private List disabledSystemFields; // schema,query,template are not part of the domain, but it's handy to pass // these values to the PropertiedEditor along with the GWTDomain. // NOTE queryName is not necessarily == name - private String schemaName=null; - private String queryName=null; - - private String templateDescription=null; // null if no template - private String instructions = null; - - private boolean supportsPhiLevel = false; + @Getter @Setter private String schemaName = null; + @Getter @Setter private String queryName = null; + @Getter @Setter private String templateDescription = null; // null if no template + @Getter @Setter private String instructions = null; + @Getter @Setter private boolean supportsPhiLevel = false; public GWTDomain() { @@ -88,7 +79,7 @@ public GWTDomain() public GWTDomain(GWTDomain src) { _ts = src._ts; - this.domainId = src.domainId; + this.domainId = src.domainId; this.name = src.name; this.domainURI = src.domainURI; this.domainKindName = src.domainKindName; @@ -107,7 +98,7 @@ public GWTDomain(GWTDomain src) this.defaultDefaultValueType = src.defaultDefaultValueType; this.defaultValueOptions = src.defaultValueOptions; this.defaultValuesURL = src.defaultValuesURL; - this.provisioned = src.isProvisioned(); + this.provisioned = src.provisioned; this.supportsPhiLevel = src.supportsPhiLevel; if (src.indices != null) @@ -158,106 +149,6 @@ public String get_Ts() return _ts; } - public int getDomainId() - { - return domainId; - } - - public void setDomainId(int domainId) - { - this.domainId = domainId; - } - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } - - public String getSchemaName() - { - return this.schemaName; - } - - public void setSchemaName(String name) - { - this.schemaName = name; - } - - public String getQueryName() - { - return this.queryName; - } - - public void setQueryName(String name) - { - this.queryName = name; - } - - public String getTemplateDescription() - { - return templateDescription; - } - - public void setTemplateDescription(String templateDescription) - { - this.templateDescription = templateDescription; - } - - public String getInstructions() - { - return instructions; - } - - public void setInstructions(String instructions) - { - this.instructions = instructions; - } - - public String getDomainURI() - { - return domainURI; - } - - public void setDomainURI(String domainURI) - { - this.domainURI = domainURI; - } - - public String getDomainKindName() - { - return domainKindName; - } - - public void setDomainKindName(String domainKindName) - { - this.domainKindName = domainKindName; - } - - public String getDescription() - { - return description; - } - - public void setDescription(String description) - { - this.description = description; - } - - public String getContainer() - { - return container; - } - - public void setContainer(String container) - { - this.container = container; - } - @JsonIgnore public List getFields(boolean includeCalculated) { @@ -300,96 +191,6 @@ public FieldType getFieldByName(String name) return null; } - public List getIndices() - { - return indices; - } - - public void setIndices(List indices) - { - this.indices = indices; - } - - public boolean isAllowFileLinkProperties() - { - return allowFileLinkProperties; - } - - public void setAllowFileLinkProperties(boolean allowFileLinkProperties) - { - this.allowFileLinkProperties = allowFileLinkProperties; - } - - public boolean isAllowAttachmentProperties() - { - return allowAttachmentProperties; - } - - public void setAllowAttachmentProperties(boolean allowAttachmentProperties) - { - this.allowAttachmentProperties = allowAttachmentProperties; - } - - public boolean isAllowFlagProperties() - { - return allowFlagProperties; - } - - public void setAllowFlagProperties(boolean allowFlagProperties) - { - this.allowFlagProperties = allowFlagProperties; - } - - public boolean isAllowTextChoiceProperties() - { - return allowTextChoiceProperties; - } - - public void setAllowTextChoiceProperties(boolean allowTextChoiceProperties) - { - this.allowTextChoiceProperties = allowTextChoiceProperties; - } - - public boolean isAllowSampleSubjectProperties() - { - return allowSampleSubjectProperties; - } - - public void setAllowSampleSubjectProperties(boolean allowSampleSubjectProperties) - { - this.allowSampleSubjectProperties = allowSampleSubjectProperties; - } - - public boolean isAllowTimepointProperties() - { - return allowTimepointProperties; - } - - public void setAllowTimepointProperties(boolean allowTimepointProperties) - { - this.allowTimepointProperties = allowTimepointProperties; - } - - public boolean isAllowUniqueConstraintProperties() - { - return allowUniqueConstraintProperties; - } - - public void setAllowUniqueConstraintProperties(boolean allowUniqueConstraintProperties) - { - this.allowUniqueConstraintProperties = allowUniqueConstraintProperties; - } - - public boolean isAllowCalculatedFields() - { - return allowCalculatedFields; - } - - public void setAllowCalculatedFields(boolean allowCalculatedFields) - { - this.allowCalculatedFields = allowCalculatedFields; - } - /** * @return Indicates that the property can't be removed from the domain. The property may or may not be nullable. */ @@ -504,16 +305,6 @@ public void setPhiNotAllowedFieldNames(Set phiNotAllowedFieldNames) } } - public boolean isShowDefaultValueSettings() - { - return showDefaultValueSettings; - } - - public void setShowDefaultValueSettings(boolean showDefaultValueSettings) - { - this.showDefaultValueSettings = showDefaultValueSettings; - } - public DefaultValueType getDefaultDefaultValueType() { return defaultDefaultValueType; @@ -530,16 +321,6 @@ public void setDefaultValueOptions(DefaultValueType[] defaultOptions, DefaultVal this.defaultValueOptions = defaultOptions; } - public List getDisabledSystemFields() - { - return disabledSystemFields; - } - - public void setDisabledSystemFields(List disabledSystemFields) - { - this.disabledSystemFields = disabledSystemFields; - } - public String getDefaultValuesURL() { if (defaultValuesURL == null) @@ -551,28 +332,4 @@ public void setDefaultValuesURL(String defaultValuesURL) { this.defaultValuesURL = defaultValuesURL; } - - /** - * Flag indicating domain is provisioned - * @return - */ - public boolean isProvisioned() - { - return provisioned; - } - - public void setProvisioned(boolean value) - { - this.provisioned = value; - } - - public boolean isSupportsPhiLevel() - { - return supportsPhiLevel; - } - - public void setSupportsPhiLevel(boolean supportsPhiLevel) - { - this.supportsPhiLevel = supportsPhiLevel; - } } diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java new file mode 100644 index 00000000000..0dc50ead244 --- /dev/null +++ b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTFilterCriteria.java @@ -0,0 +1,34 @@ +package org.labkey.api.gwt.client.model; + +import com.google.gwt.user.client.rpc.IsSerializable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Setter +@Getter +@EqualsAndHashCode +public class GWTFilterCriteria implements Serializable, IsSerializable +{ + private String name; + private String op; + private Integer propertyId; + private Integer referencePropertyId; + private Object value; + + public GWTFilterCriteria() + { + } + + public GWTFilterCriteria(GWTFilterCriteria fc) + { + setName(fc.getName()); + setOp(fc.getOp()); + setPropertyId(fc.getPropertyId()); + setReferencePropertyId(fc.getReferencePropertyId()); + setValue(fc.getValue()); + } +} + diff --git a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java index 82977e8b57a..d81c5af4cac 100644 --- a/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java +++ b/api/gwtsrc/org/labkey/api/gwt/client/model/GWTPropertyDescriptor.java @@ -17,6 +17,10 @@ package org.labkey.api.gwt.client.model; import com.google.gwt.user.client.rpc.IsSerializable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; import org.labkey.api.gwt.client.DefaultScaleType; import org.labkey.api.gwt.client.DefaultValueType; import org.labkey.api.gwt.client.LockedPropertyType; @@ -29,67 +33,60 @@ import java.util.List; /** - * User: matthewb - * Date: Apr 24, 2007 - * Time: 1:28:42 PM - * - * see org.labkey.api.exp.PropertyDescriptor + * See {@link org.labkey.api.exp.PropertyDescriptor} */ +@EqualsAndHashCode public class GWTPropertyDescriptor implements IsSerializable { - private IntegerProperty propertyId = new IntegerProperty(0); - private StringProperty propertyURI = new StringProperty(); - private StringProperty container = new StringProperty(); - private StringProperty name = new StringProperty(); - private StringProperty description = new StringProperty(); - private StringProperty rangeURI = new StringProperty("http://www.w3.org/2001/XMLSchema#string"); - private StringProperty conceptURI = new StringProperty(); - private StringProperty label = new StringProperty(); - private StringProperty format = new StringProperty(); - private BooleanProperty required = new BooleanProperty(false); - private BooleanProperty hidden = new BooleanProperty(false); - private StringProperty lookupContainer = new StringProperty(); - private StringProperty lookupSchema = new StringProperty(); - private StringProperty lookupQuery = new StringProperty(); - private BooleanProperty lookupIsValid = new BooleanProperty(true); + private final IntegerProperty propertyId = new IntegerProperty(0); + private final StringProperty propertyURI = new StringProperty(); + private final StringProperty container = new StringProperty(); + private final StringProperty name = new StringProperty(); + private final StringProperty description = new StringProperty(); + private final StringProperty rangeURI = new StringProperty("http://www.w3.org/2001/XMLSchema#string"); + private final StringProperty conceptURI = new StringProperty(); + private final StringProperty label = new StringProperty(); + private final StringProperty format = new StringProperty(); + private final BooleanProperty required = new BooleanProperty(false); + private final BooleanProperty hidden = new BooleanProperty(false); + private final StringProperty lookupContainer = new StringProperty(); + private final StringProperty lookupSchema = new StringProperty(); + private final StringProperty lookupQuery = new StringProperty(); + private final BooleanProperty lookupIsValid = new BooleanProperty(true); private String defaultValueType = null; - private StringProperty defaultValue = new StringProperty(); - private StringProperty defaultDisplayValue = new StringProperty("[none]"); - private BooleanProperty mvEnabled = new BooleanProperty(false); - private StringProperty importAliases = new StringProperty(); - private StringProperty url = new StringProperty(); - private BooleanProperty shownInInsertView = new BooleanProperty(true); - private BooleanProperty shownInUpdateView = new BooleanProperty(true); - private BooleanProperty shownInDetailsView = new BooleanProperty(true); - private BooleanProperty measure = new BooleanProperty(); - private BooleanProperty dimension = new BooleanProperty(); - private BooleanProperty recommendedVariable = new BooleanProperty(false); - private StringProperty defaultScale = new StringProperty(DefaultScaleType.LINEAR.name()); - private StringProperty facetingBehaviorType = new StringProperty(); - private StringProperty phi = new StringProperty("NotPHI"); // Must match PHI.NotPHI and tableInfo.xsd enum PHIType.NotPHI - private BooleanProperty isExcludeFromShifting = new BooleanProperty(); - private BooleanProperty isPreventReordering = new BooleanProperty(); - private BooleanProperty isDisableEditing = new BooleanProperty(); - private IntegerProperty scale = new IntegerProperty(4000); - private StringProperty principalConceptCode = new StringProperty(); - private StringProperty sourceOntology = new StringProperty(); - private StringProperty conceptSubtree = new StringProperty(); - private StringProperty conceptImportColumn = new StringProperty(); - private StringProperty conceptLabelColumn = new StringProperty(); - private StringProperty redactedText = new StringProperty(); - private StringProperty derivationDataScope = new StringProperty(); - private BooleanProperty isPrimaryKey = new BooleanProperty(false); - private StringProperty lockType = new StringProperty(LockedPropertyType.NotLocked.name()); - private BooleanProperty scannable = new BooleanProperty(false); - private StringProperty valueExpression = new StringProperty(); - - // for controlling the property editor (not persisted or user settable) -// private boolean isEditable = true; - private boolean isTypeEditable = true; -// private boolean isNameEditable = true; - - private List validators = new ArrayList(); - private List conditionalFormats = new ArrayList(); + private final StringProperty defaultValue = new StringProperty(); + private final StringProperty defaultDisplayValue = new StringProperty("[none]"); + private final BooleanProperty mvEnabled = new BooleanProperty(false); + private final StringProperty importAliases = new StringProperty(); + private final StringProperty url = new StringProperty(); + private final BooleanProperty shownInInsertView = new BooleanProperty(true); + private final BooleanProperty shownInUpdateView = new BooleanProperty(true); + private final BooleanProperty shownInDetailsView = new BooleanProperty(true); + private final BooleanProperty measure = new BooleanProperty(); + private final BooleanProperty dimension = new BooleanProperty(); + private final BooleanProperty recommendedVariable = new BooleanProperty(false); + private final StringProperty defaultScale = new StringProperty(DefaultScaleType.LINEAR.name()); + private final StringProperty facetingBehaviorType = new StringProperty(); + private final StringProperty phi = new StringProperty("NotPHI"); // Must match PHI.NotPHI and tableInfo.xsd enum PHIType.NotPHI + private final BooleanProperty isExcludeFromShifting = new BooleanProperty(); + private final BooleanProperty isPreventReordering = new BooleanProperty(); + private final BooleanProperty isDisableEditing = new BooleanProperty(); + private final IntegerProperty scale = new IntegerProperty(4000); + private final StringProperty principalConceptCode = new StringProperty(); + private final StringProperty sourceOntology = new StringProperty(); + private final StringProperty conceptSubtree = new StringProperty(); + private final StringProperty conceptImportColumn = new StringProperty(); + private final StringProperty conceptLabelColumn = new StringProperty(); + private final StringProperty redactedText = new StringProperty(); + private final StringProperty derivationDataScope = new StringProperty(); + private final BooleanProperty isPrimaryKey = new BooleanProperty(false); + private final StringProperty lockType = new StringProperty(LockedPropertyType.NotLocked.name()); + private final BooleanProperty scannable = new BooleanProperty(false); + private final StringProperty valueExpression = new StringProperty(); + + @Getter @Setter private List conditionalFormats = new ArrayList<>(); + @Getter @Setter private List filterCriteria = new ArrayList<>(); + @Getter @Setter private List propertyValidators = new ArrayList<>(); public GWTPropertyDescriptor() { @@ -123,6 +120,7 @@ public GWTPropertyDescriptor(GWTPropertyDescriptor s) setRecommendedVariable(s.isRecommendedVariable()); setDefaultScale(s.getDefaultScale()); setLookupContainer(s.getLookupContainer()); + setLookupIsValid(s.getLookupIsValid()); setLookupSchema(s.getLookupSchema()); setLookupQuery(s.getLookupQuery()); setDefaultValueType(s.getDefaultValueType()); @@ -139,7 +137,6 @@ public GWTPropertyDescriptor(GWTPropertyDescriptor s) setRedactedText(s.getRedactedText()); setIsPrimaryKey(s.getIsPrimaryKey()); setLockType(s.getLockType()); - setTypeEditable(s.isTypeEditable()); setPrincipalConceptCode(s.getPrincipalConceptCode()); setSourceOntology(s.getSourceOntology()); setConceptSubtree(s.getConceptSubtree()); @@ -151,13 +148,18 @@ public GWTPropertyDescriptor(GWTPropertyDescriptor s) for (GWTPropertyValidator v : s.getPropertyValidators()) { - validators.add(new GWTPropertyValidator(v)); + propertyValidators.add(new GWTPropertyValidator(v)); } for (GWTConditionalFormat f : s.getConditionalFormats()) { conditionalFormats.add(new GWTConditionalFormat(f)); } + + for (GWTFilterCriteria fc : s.getFilterCriteria()) + { + filterCriteria.add(new GWTFilterCriteria(fc)); + } } public GWTPropertyDescriptor copy() @@ -275,7 +277,6 @@ public void guessMeasureAndDimension() setDimension(getLookupQuery() != null && !isHidden()); } - public String getConceptURI() { return conceptURI.getString(); @@ -626,145 +627,6 @@ public String debugString() return getName() + " " + getLabel() + " " + getRangeURI() + " " + isRequired() + " " + getDescription(); } - private boolean equals(String a, String b) - { - if (null == a || null == b) - return a==b; - return a.equals(b); - } - - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null) return false; - if (!(o instanceof GWTPropertyDescriptor)) - return false; - - GWTPropertyDescriptor that = (GWTPropertyDescriptor) o; - - if (getPropertyId() != that.getPropertyId()) return false; - if (isRequired() != that.isRequired()) return false; - if (isHidden() != that.isHidden()) return false; - if (getMvEnabled() != that.getMvEnabled()) return false; - if (getContainer() != null ? !getContainer().equals(that.getContainer()) : that.getContainer() != null) return false; - if (getConceptURI() != null ? !getConceptURI().equals(that.getConceptURI()) : that.getConceptURI() != null) return false; - if (getDescription() != null ? !getDescription().equals(that.getDescription()) : that.getDescription() != null) return false; - if (getFormat() != null ? !getFormat().equals(that.getFormat()) : that.getFormat() != null) return false; - if (getLabel() != null ? !getLabel().equals(that.getLabel()) : that.getLabel() != null) return false; - if (getLookupContainer() != null ? !getLookupContainer().equals(that.getLookupContainer()) : that.getLookupContainer() != null) - return false; - if (getLookupQuery() != null ? !getLookupQuery().equals(that.getLookupQuery()) : that.getLookupQuery() != null) return false; - if (getLookupSchema() != null ? !getLookupSchema().equals(that.getLookupSchema()) : that.getLookupSchema() != null) return false; - if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) return false; - if (getPropertyURI() != null ? !getPropertyURI().equals(that.getPropertyURI()) : that.getPropertyURI() != null) return false; - if (getRangeURI() != null ? !getRangeURI().equals(that.getRangeURI()) : that.getRangeURI() != null) return false; - if (getDefaultValueType() != null ? !getDefaultValueType().equals(that.getDefaultValueType()) : that.getDefaultValueType() != null) return false; - if (getDefaultValue() != null ? !getDefaultValue().equals(that.getDefaultValue()) : that.getDefaultValue() != null) return false; - if (getDefaultDisplayValue() != null ? !getDefaultDisplayValue().equals(that.getDefaultDisplayValue()) : that.getDefaultDisplayValue() != null) return false; - if (getImportAliases() != null ? !getImportAliases().equals(that.getImportAliases()) : that.getImportAliases() != null) return false; - if (getURL() != null ? !getURL().equals(that.getURL()) : that.getURL() != null) return false; - if (isShownInDetailsView() != that.isShownInDetailsView()) return false; - if (isShownInInsertView() != that.isShownInInsertView()) return false; - if (isShownInUpdateView() != that.isShownInUpdateView()) return false; - if (isMeasure() != that.isMeasure()) return false; - if (isDimension() != that.isDimension()) return false; - if (isRecommendedVariable() != that.isRecommendedVariable()) return false; - if (!equals(getDefaultScale(), that.getDefaultScale())) return false; - if (!equals(getFacetingBehaviorType(), that.getFacetingBehaviorType())) return false; - if (getPHI() != null ? !getPHI().equals(that.getPHI()) : that.getPHI() != null) return false; - if (isExcludeFromShifting() != that.isExcludeFromShifting()) return false; - - if (!getPropertyValidators().equals(that.getPropertyValidators())) return false; - if (!getConditionalFormats().equals(that.getConditionalFormats())) - { - return false; - } - if (!getScale().equals(that.getScale())) return false; - - if (!equals(getPrincipalConceptCode(),that.getPrincipalConceptCode())) return false; - if (!equals(getSourceOntology(),that.getSourceOntology())) return false; - if (!equals(getConceptSubtree(), that.getConceptSubtree())) return false; - if (!equals(getConceptImportColumn(),that.getConceptImportColumn())) return false; - if (!equals(getConceptLabelColumn(),that.getConceptLabelColumn())) return false; - if (!equals(getDerivationDataScope(),that.getDerivationDataScope())) return false; - if (getRedactedText() != null ? !getRedactedText().equals(that.getRedactedText()) : that.getRedactedText() != null) return false; - if (isScannable() != that.isScannable()) return false; - if (!equals(getValueExpression(),that.getValueExpression())) return false; - - return true; - } - - public int hashCode() - { - int result; - result = (propertyId.getInteger() != null ? propertyId.getInteger().hashCode() : 0); - result = 31 * result + (propertyURI.getString() != null ? propertyURI.getString().hashCode() : 0); - result = 31 * result + (name.getString() != null ? name.getString().hashCode() : 0); - result = 31 * result + (description.getString() != null ? description.getString().hashCode() : 0); - result = 31 * result + (container.getString() != null ? container.getString().hashCode() : 0); - result = 31 * result + (rangeURI.getString() != null ? rangeURI.getString().hashCode() : 0); - result = 31 * result + (conceptURI.getString() != null ? conceptURI.getString().hashCode() : 0); - result = 31 * result + (label.getString() != null ? label.getString().hashCode() : 0); - result = 31 * result + (format.getString() != null ? format.getString().hashCode() : 0); - result = 31 * result + (required.getBoolean() != null ? required.getBoolean().hashCode() : 0); - result = 31 * result + (hidden.getBoolean() != null ? hidden.getBoolean().hashCode() : 0); - result = 31 * result + (mvEnabled.getBoolean() != null ? mvEnabled.getBoolean().hashCode() : 0); - result = 31 * result + (lookupContainer.getString() != null ? lookupContainer.getString().hashCode() : 0); - result = 31 * result + (lookupSchema.getString() != null ? lookupSchema.getString().hashCode() : 0); - result = 31 * result + (lookupQuery.getString() != null ? lookupQuery.getString().hashCode() : 0); - result = 31 * result + (defaultValueType != null ? defaultValueType.hashCode() : 0); - result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); - result = 31 * result + (defaultDisplayValue != null ? defaultDisplayValue.hashCode() : 0); - result = 31 * result + (url != null ? url.hashCode() : 0); - result = 31 * result + (importAliases != null ? importAliases.hashCode() : 0); - result = 31 * result + (shownInDetailsView.getBoolean() != null ? shownInDetailsView.getBoolean().hashCode() : 0); - result = 31 * result + (shownInInsertView.getBoolean() != null ? shownInInsertView.getBoolean().hashCode() : 0); - result = 31 * result + (shownInUpdateView.getBoolean() != null ? shownInUpdateView.getBoolean().hashCode() : 0); - result = 31 * result + (dimension.getBoolean() != null ? dimension.getBoolean().hashCode() : 0); - result = 31 * result + (measure.getBoolean() != null ? measure.getBoolean().hashCode() : 0); - result = 31 * result + (recommendedVariable.getBoolean() != null ? recommendedVariable.getBoolean().hashCode() : 0); - result = 31 * result + (defaultScale.getString() != null ? defaultScale.getString().hashCode() : 0); - result = 31 * result + (facetingBehaviorType.getString() != null ? facetingBehaviorType.getString().hashCode() : 0); - result = 31 * result + (phi.getString() != null ? phi.hashCode() : 0); - result = 31 * result + (isExcludeFromShifting.getBoolean() != null ? isExcludeFromShifting.getBoolean().hashCode() : 0); - result = 31 * result + (scale.getInteger() != null ? scale.getInteger().hashCode() : 0); - result = 31 * result + sourceOntology.hashCode(); - result = 31 * result + conceptSubtree.hashCode(); - result = 31 * result + conceptImportColumn.hashCode(); - result = 31 * result + conceptLabelColumn.hashCode(); - result = 31 * result + principalConceptCode.hashCode(); - result = 31 * result + redactedText.hashCode(); - result = 31 * result + derivationDataScope.hashCode(); - result = 31 * result + (scannable.getBoolean() != null ? scannable.getBoolean().hashCode() : 0); - result = 31 * result + valueExpression.hashCode(); - - for (GWTPropertyValidator gwtPropertyValidator : getPropertyValidators()) - { - result = 31 * result + gwtPropertyValidator.hashCode(); - } - return result; - } - - public List getPropertyValidators() - { - return validators; - } - - public void setPropertyValidators(List validators) - { - this.validators = validators; - } - - public List getConditionalFormats() - { - return conditionalFormats; - } - - public void setConditionalFormats(List conditionalFormats) - { - this.conditionalFormats = conditionalFormats; - } - public String getImportAliases() { return importAliases.toString(); @@ -787,12 +649,10 @@ public void setURL(String url) public String getLookupDescription() { - if (getLookupQuery() != null && getLookupQuery().length() > 0 && - getLookupSchema() != null && getLookupSchema().length() > 0) - { - return getLookupSchema() + "." + getLookupQuery(); - } - return "(none)"; + if (StringUtils.isEmpty(getLookupSchema()) || StringUtils.isEmpty(getLookupQuery())) + return "(none)"; + + return getLookupSchema() + "." + getLookupQuery(); } @Override @@ -806,36 +666,4 @@ public boolean isFileType() return "http://cpas.fhcrc.org/exp/xml#fileLink".equals(getRangeURI()) || "http://www.labkey.org/exp/xml#attachment".equals(getRangeURI()); } - - - // for communicating with the type editor, not persisted -// public void setEditable(boolean b) -// { -// isEditable = b; -// } -// -// public boolean isEditable() -// { -// return isEditable; -// } - - public void setTypeEditable(boolean b) - { - isTypeEditable = b; - } - - public boolean isTypeEditable() - { - return isTypeEditable; - } - -// public void setNameEditable(boolean b) -// { -// isNameEditable = b; -// } -// -// public boolean isNameEditable() -// { -// return isNameEditable; -// } } diff --git a/api/src/org/labkey/api/assay/AbstractAssayProvider.java b/api/src/org/labkey/api/assay/AbstractAssayProvider.java index 0a138def90d..09d707bd205 100644 --- a/api/src/org/labkey/api/assay/AbstractAssayProvider.java +++ b/api/src/org/labkey/api/assay/AbstractAssayProvider.java @@ -24,6 +24,7 @@ import org.labkey.api.assay.actions.DesignerAction; import org.labkey.api.assay.actions.UploadWizardAction; import org.labkey.api.assay.pipeline.AssayRunAsyncContext; +import org.labkey.api.assay.plate.FilterCriteria; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.audit.AuditLogService; import org.labkey.api.data.ActionButton; @@ -400,8 +401,18 @@ public Domain getResultsDomain(ExpProtocol protocol) return getDomainByPrefix(protocol, ExpProtocol.ASSAY_DOMAIN_DATA); } + protected @Nullable Domain getResultsDomainIfExists(ExpProtocol protocol) + { + return getDomainByPrefixIfExists(protocol, ExpProtocol.ASSAY_DOMAIN_DATA); + } + @Override - public void changeDomain(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) + public void beforeDomainChange(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException + { + } + + @Override + public void afterDomainChange(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException { } @@ -1349,7 +1360,7 @@ public List getValidationAndAnalysisScripts(ExpProtocol protocol, Scope sc } @Override - public void setSaveScriptFiles(ExpProtocol protocol, boolean save) throws ExperimentException + public void setSaveScriptFiles(ExpProtocol protocol, boolean save) { setBooleanProperty(protocol, SAVE_SCRIPT_FILES_PROPERTY_SUFFIX, save); } @@ -1453,12 +1464,9 @@ protected Boolean getBooleanProperty(ExpProtocol protocol, String propertySuffix { ObjectProperty prop = protocol.getObjectProperties().get(createPropertyURI(protocol, propertySuffix)); - if (prop != null) - { - Object o = prop.value(); - if (o instanceof Boolean) - return (Boolean)o; - } + if (prop != null && prop.value() instanceof Boolean b) + return b; + return null; } @@ -1747,6 +1755,41 @@ public boolean isPlateMetadataEnabled(ExpProtocol protocol) return supportsPlateMetadata(protocol) && Boolean.TRUE.equals(getBooleanProperty(protocol, PLATE_METADATA_PROPERTY_SUFFIX)); } + @Override + public @NotNull List getFilterCriteria(ExpProtocol protocol) + { + Domain resultsDomain = getResultsDomainIfExists(protocol); + if (resultsDomain == null) + return Collections.emptyList(); + + return getFilterCriteria(protocol, resultsDomain); + } + + protected @NotNull List getFilterCriteria(ExpProtocol protocol, Domain resultsDomain) + { + return Collections.emptyList(); + } + + @Override + public boolean hasFilterCriteria(ExpProtocol protocol) + { + Domain resultsDomain = getResultsDomainIfExists(protocol); + if (resultsDomain == null) + return false; + + return hasFilterCriteria(protocol, resultsDomain); + } + + protected boolean hasFilterCriteria(ExpProtocol protocol, Domain resultsDomain) + { + return false; + } + + @Override + public void removeFilterCriteriaForProperty(PropertyDescriptor pd) + { + } + public record AssayFileMoveData(ExpRun run, Container sourceContainer, String fieldName, File sourceFile, File targetFile) {} public record AssayMoveData(Map counts, Map> fileMovesByRunId) {} diff --git a/api/src/org/labkey/api/assay/AssayDomainService.java b/api/src/org/labkey/api/assay/AssayDomainService.java index ab81f5d8fe1..ee47049f1f0 100644 --- a/api/src/org/labkey/api/assay/AssayDomainService.java +++ b/api/src/org/labkey/api/assay/AssayDomainService.java @@ -25,5 +25,5 @@ public interface AssayDomainService GWTProtocol getAssayTemplate(String providerName); - GWTProtocol saveChanges(GWTProtocol plate, boolean replaceIfExisting) throws ValidationException; + GWTProtocol saveChanges(GWTProtocol protocol, boolean replaceIfExisting) throws ValidationException; } \ No newline at end of file diff --git a/api/src/org/labkey/api/assay/AssayProvider.java b/api/src/org/labkey/api/assay/AssayProvider.java index b3c912ab681..983737614a7 100644 --- a/api/src/org/labkey/api/assay/AssayProvider.java +++ b/api/src/org/labkey/api/assay/AssayProvider.java @@ -21,12 +21,14 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.actions.AssayRunUploadForm; import org.labkey.api.assay.pipeline.AssayRunAsyncContext; +import org.labkey.api.assay.plate.FilterCriteria; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.Handler; import org.labkey.api.exp.Lsid; import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.XarContext; import org.labkey.api.exp.api.ExpData; import org.labkey.api.exp.api.ExpExperiment; @@ -100,7 +102,8 @@ enum ReRunSupport Domain getResultsDomain(ExpProtocol protocol); - void changeDomain(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update); + void beforeDomainChange(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException; + void afterDomainChange(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException; AssayRunCreator getRunCreator(); @@ -274,6 +277,10 @@ enum Scope void setPlateMetadataEnabled(ExpProtocol protocol, boolean metadataEnabled); boolean isPlateMetadataEnabled(ExpProtocol protocol); + @NotNull List getFilterCriteria(ExpProtocol protocol); + boolean hasFilterCriteria(ExpProtocol protocol); + void removeFilterCriteriaForProperty(PropertyDescriptor pd); + /** * @return the data type that this run creates for its analyzed results */ diff --git a/api/src/org/labkey/api/assay/AssayService.java b/api/src/org/labkey/api/assay/AssayService.java index 64e443e5223..826817c8894 100644 --- a/api/src/org/labkey/api/assay/AssayService.java +++ b/api/src/org/labkey/api/assay/AssayService.java @@ -80,7 +80,7 @@ static void setInstance(AssayService impl) @NotNull Collection getAssayProviders(); - WebPartView createAssayListView(ViewContext context, boolean portalView, BindException errors); + WebPartView createAssayListView(ViewContext context, boolean portalView, BindException errors); ExpRunTable createRunTable(ExpProtocol protocol, AssayProvider provider, User user, Container container, ContainerFilter cf); diff --git a/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java b/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java index ac88c481d51..535e53a2d66 100644 --- a/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java +++ b/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java @@ -6,6 +6,7 @@ import org.labkey.api.assay.AssayRunUploadContext; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.TableInfo; import org.labkey.api.dataiterator.DataIteratorBuilder; import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.Lsid; @@ -17,6 +18,7 @@ import org.labkey.api.gwt.client.model.GWTDomain; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.qc.DataLoaderSettings; +import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.services.ServiceRegistry; import org.labkey.vfs.FileLike; @@ -27,6 +29,7 @@ public interface AssayPlateMetadataService { String PLATE_SET_COLUMN_NAME = "PlateSet"; + String HIT_SELECTION_CRITERIA_COLUMN_NAME = "HitSelectionCriteria"; static void setInstance(AssayPlateMetadataService serviceImpl) { @@ -46,6 +49,9 @@ static AssayPlateMetadataService get() return ServiceRegistry.get().getService(AssayPlateMetadataService.class); } + Map> previewFilterCriteriaColumns(@NotNull ExpProtocol protocol, List columnNames); + Map> previewFilterCriteriaColumns(@NotNull Container container, String protocolName, List columnNames); + /** * Merges the results data with the plate metadata to produce a single row map * @@ -104,9 +110,9 @@ OntologyManager.UpdateableTableImportHelper getImportHelper( void updateReplicateStatsDomain( User user, ExpProtocol protocol, - GWTDomain update, - Domain resultsDomain - ) throws ExperimentException; + GWTDomain original, + GWTDomain update + ) throws ValidationException; /** * Computes and inserts replicate statistics into the protocol schema table. @@ -120,19 +126,27 @@ void insertReplicateStats( ExpProtocol protocol, @NotNull ExpRun run, Map>> replicateRows - ) throws ExperimentException; + ) throws ValidationException; void updateReplicateStats( Container container, User user, ExpProtocol protocol, Map>> replicateRows - ) throws ExperimentException; + ) throws ValidationException; void deleteReplicateStats( Container container, User user, ExpProtocol protocol, List> keys - ) throws ExperimentException; + ) throws ValidationException; + + void applyHitSelectionCriteria( + Container container, + User user, + ExpProtocol protocol, + TableInfo resultsTable, + List runIds + ) throws ValidationException; } diff --git a/api/src/org/labkey/api/assay/plate/FilterCriteria.java b/api/src/org/labkey/api/assay/plate/FilterCriteria.java new file mode 100644 index 00000000000..686c882bcac --- /dev/null +++ b/api/src/org/labkey/api/assay/plate/FilterCriteria.java @@ -0,0 +1,141 @@ +package org.labkey.api.assay.plate; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.CompareType; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.gwt.client.model.GWTFilterCriteria; +import org.labkey.api.query.ValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public record FilterCriteria( + String operation, + String value, + Integer propertyId, + @Nullable String name, + Integer referencePropertyId, + Integer domainId +) +{ + public static @NotNull List fromGWTFilterCriteria( + List filterCriteria, + int referencePropertyId, + String referencePropertyName, + int domainId, + @Nullable Domain replicateStatsDomain + ) throws ValidationException + { + if (filterCriteria == null || filterCriteria.isEmpty()) + return Collections.emptyList(); + + // Invariants + if (referencePropertyId <= 0) + throw new IllegalArgumentException("A valid \"referencePropertyId\" must be specified for filter criteria."); + if (StringUtils.trimToNull(referencePropertyName) == null) + throw new IllegalArgumentException("A valid \"referencePropertyName\" must be specified for filter criteria."); + if (domainId <= 0) + throw new IllegalArgumentException("A valid \"domainId\" must be specified for filter criteria."); + + var criteria = new ArrayList(); + + for (int i = 0; i < filterCriteria.size(); i++) + { + var criterion = filterCriteria.get(i); + var propertyId = criterion.getPropertyId(); + + if (propertyId != null) + { + if (propertyId == 0) + throw new ValidationException(errorMessage(referencePropertyName, i, "Invalid \"propertyId\" value.")); + else if (propertyId < 0) + propertyId = null; + } + + String name = StringUtils.trimToNull(criterion.getName()); + + if (propertyId == null) + { + // Attempt to resolve the field by name + if (name != null) + { + if (name.equalsIgnoreCase(referencePropertyName)) + { + propertyId = referencePropertyId; + name = referencePropertyName; + } + else if (replicateStatsDomain != null) + { + var property = replicateStatsDomain.getPropertyByName(name); + if (property != null) + { + propertyId = property.getPropertyId(); + name = property.getName(); + } + } + + if (propertyId == null) + throw new ValidationException(errorMessage(referencePropertyName, i, String.format("Unable to resolve field from name \"%s\".", name))); + } + } + else + { + if (propertyId == referencePropertyId) + name = referencePropertyName; + else if (replicateStatsDomain != null) + { + var property = replicateStatsDomain.getProperty(propertyId); + if (property == null) + throw new ValidationException(errorMessage(referencePropertyName, i, "Invalid \"propertyId\" value. Cannot specify criteria against other fields.")); + } + } + + if (propertyId == null) + { + propertyId = referencePropertyId; + name = referencePropertyName; + } + + String operation = StringUtils.trimToNull(criterion.getOp()); + if (operation == null) + throw new ValidationException(errorMessage(referencePropertyName, i, "An \"op\" (operation) property is required.")); + if (CompareType.getByURLKey(operation) == null) + throw new ValidationException(errorMessage(referencePropertyName, i, String.format("\"%s\" is not a valid operation.", operation))); + + String value = criterion.getValue() == null ? null : criterion.getValue().toString(); + criteria.add(new FilterCriteria(operation, value, propertyId, name, referencePropertyId, domainId)); + } + + return criteria; + } + + public static List toGWTFilterCriteria(List criteria) + { + if (criteria == null || criteria.isEmpty()) + return Collections.emptyList(); + + var filterCriteria = new ArrayList(); + + for (var criterion : criteria) + { + var filterCriterion = new GWTFilterCriteria(); + filterCriterion.setName(criterion.name); + filterCriterion.setOp(criterion.operation); + filterCriterion.setPropertyId(criterion.propertyId); + filterCriterion.setReferencePropertyId(criterion.referencePropertyId); + filterCriterion.setValue(criterion.value); + + filterCriteria.add(filterCriterion); + } + + return filterCriteria; + } + + private static String errorMessage(String referencePropertyName, int index, String message) + { + return String.format("Invalid hit criteria for field \"%s\" at index [%d]. %s", referencePropertyName, index, message); + } +} diff --git a/api/src/org/labkey/api/data/CompareType.java b/api/src/org/labkey/api/data/CompareType.java index 11568942838..1b0187eeac8 100644 --- a/api/src/org/labkey/api/data/CompareType.java +++ b/api/src/org/labkey/api/data/CompareType.java @@ -818,7 +818,7 @@ private static class QClause extends CompareType.CompareClause } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { sb.append("Search for \""); sb.append(getParamVals()[0]); @@ -1667,7 +1667,7 @@ String toWhereClause(SqlDialect dialect, String alias) } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { sb.append("DATE("); appendColumnName(sb, formatter); @@ -2229,7 +2229,7 @@ public String getLabKeySQLWhereClause(Map column } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { // Try to resolve the parameter value to a Group or User object diff --git a/api/src/org/labkey/api/data/SimpleFilter.java b/api/src/org/labkey/api/data/SimpleFilter.java index 4d8f27febfa..5a2112d0354 100644 --- a/api/src/org/labkey/api/data/SimpleFilter.java +++ b/api/src/org/labkey/api/data/SimpleFilter.java @@ -163,7 +163,7 @@ public Object[] getParamVals() return _paramVals; } - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { int fromIndex = appendFilterValueText(sb, formatter); replaceParamValues(sb, fromIndex); @@ -458,7 +458,7 @@ public String getLabKeySQLWhereClause(Map column } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { String sep = ""; for (FilterClause clause : _clauses) @@ -661,7 +661,7 @@ public InClause(FieldKey fieldKey, String namedSet, boolean urlClause) } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { sb.append(formatter.format(getFieldKey())); @@ -848,7 +848,7 @@ public ContainsOneOfClause(FieldKey fieldKey, Collection params, boolean urlC } @Override - protected void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, ColumnNameFormatter formatter) { sb.append(formatter.format(getFieldKey())); sb.append(" ").append(isNegated() ? "DOES NOT CONTAIN ANY OF " : "CONTAINS ONE OF "); diff --git a/api/src/org/labkey/api/exp/property/DomainUtil.java b/api/src/org/labkey/api/exp/property/DomainUtil.java index fb8ec6a4506..d26c1e3af96 100644 --- a/api/src/org/labkey/api/exp/property/DomainUtil.java +++ b/api/src/org/labkey/api/exp/property/DomainUtil.java @@ -677,17 +677,19 @@ public static GWTPropertyDescriptor getPropertyDescriptor(ColumnType columnXml) return gwtProp; } - public static Domain createDomain(DomainTemplate template, Container container, User user, @Nullable String domainName) throws ValidationException { return createDomain(template.getDomainKind(), template.getDomain(), template.getOptions(), container, user, domainName, template.getTemplateInfo()); } - public static Domain createDomain( - String kindName, GWTDomain domain, Map arguments, - Container container, User user, @Nullable String domainName, - @Nullable TemplateInfo templateInfo + String kindName, + GWTDomain domain, + Map arguments, + Container container, + User user, + @Nullable String domainName, + @Nullable TemplateInfo templateInfo ) throws ValidationException { // Create a copy of the GWTDomain to ensure the template's Domain is not modified diff --git a/api/src/org/labkey/api/query/MetadataColumnJSON.java b/api/src/org/labkey/api/query/MetadataColumnJSON.java index c856bcf018e..b49aaa9a8a7 100644 --- a/api/src/org/labkey/api/query/MetadataColumnJSON.java +++ b/api/src/org/labkey/api/query/MetadataColumnJSON.java @@ -15,14 +15,10 @@ */ package org.labkey.api.query; +import lombok.EqualsAndHashCode; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; -import java.util.Objects; - -/** - * User: jeckels - * Date: Nov 14, 2008 - */ +@EqualsAndHashCode(callSuper = true) public class MetadataColumnJSON extends GWTPropertyDescriptor { private String _wrappedColumnName; @@ -41,6 +37,7 @@ public MetadataColumnJSON(MetadataColumnJSON ci) setWrappedColumnName(ci.getWrappedColumnName()); setValueExpression(ci.getValueExpression()); setLookupCustom(ci.isLookupCustom()); + setLockExistingField(ci.isLockExistingField()); } public MetadataColumnJSON(GWTPropertyDescriptor ci) @@ -59,11 +56,13 @@ public void setWrappedColumnName(String wrappedColumnName) _wrappedColumnName = wrappedColumnName; } + @Override public String getValueExpression() { return _valueExpression; } + @Override public void setValueExpression(String valueExpression) { _valueExpression = valueExpression; @@ -113,33 +112,7 @@ public void setLookupQuery(String lookupQuery) public String getLookupDescription() { if (_lookupCustom) - { return "(custom)"; - } return super.getLookupDescription(); } - - @Override - public boolean equals(Object o) - { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - MetadataColumnJSON that = (MetadataColumnJSON)o; - - if (_lookupCustom != that._lookupCustom) return false; - if (!Objects.equals(_valueExpression, that._valueExpression)) return false; - return Objects.equals(_wrappedColumnName, that._wrappedColumnName); - } - - @Override - public int hashCode() - { - int result = super.hashCode(); - result = 31 * result + (_wrappedColumnName != null ? _wrappedColumnName.hashCode() : 0); - result = 31 * result + (_valueExpression != null ? _valueExpression.hashCode() : 0); - result = 31 * result + (_lookupCustom ? 1 : 0); - return result; - } } \ No newline at end of file diff --git a/api/src/org/labkey/api/query/ValidationException.java b/api/src/org/labkey/api/query/ValidationException.java index af558adeda0..69bf3032325 100644 --- a/api/src/org/labkey/api/query/ValidationException.java +++ b/api/src/org/labkey/api/query/ValidationException.java @@ -35,19 +35,13 @@ import java.util.Map; import java.util.Set; -/* - * User: Dave - * Date: Jun 9, 2008 - * Time: 4:49:33 PM - */ - /** * This class is thrown if there were validation errors during a save. * This class is essentially a container for objects that implement * ValidationError, so use the getErrors() method to * retrieve individual validation errors. The toString() * method will simply concatenate all the error messages together, - * separated by semi-colons. + * separated by semicolons. */ public class ValidationException extends Exception implements Iterable { @@ -56,8 +50,8 @@ public class ValidationException extends Exception implements Iterable> _fieldErrors = new LinkedHashMap<>(); - private List _globalErrors = new ArrayList<>(); + private final Map> _fieldErrors = new LinkedHashMap<>(); + private final List _globalErrors = new ArrayList<>(); private String _schemaName; private String _queryName; @@ -85,8 +79,8 @@ public enum SEVERITY WARN("Warning", Level.WARN), INFO("Info", Level.INFO); - String _sevName; - Level _level; + final String _sevName; + final Level _level; SEVERITY(String sevName, Level level) { @@ -293,10 +287,10 @@ else if (value instanceof Object[]) public ValidationException addError(ValidationError error) { - if (error instanceof PropertyValidationError) - addFieldError((PropertyValidationError) error); - else if (error instanceof SimpleValidationError) - addGlobalError((SimpleValidationError) error); + if (error instanceof PropertyValidationError pve) + addFieldError(pve); + else if (error instanceof SimpleValidationError sve) + addGlobalError(sve); else throw new IllegalArgumentException(); diff --git a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java index 2ff9e63523e..d7f864bad44 100644 --- a/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java +++ b/assay/api-src/org/labkey/api/assay/AssayResultDomainKind.java @@ -22,8 +22,11 @@ import org.labkey.api.data.DbScope; import org.labkey.api.data.JdbcType; import org.labkey.api.data.PropertyStorageSpec; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.api.ExpProtocol; import org.labkey.api.exp.property.Domain; +import org.labkey.api.query.FieldKey; import org.labkey.api.security.User; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Pair; @@ -39,11 +42,20 @@ public class AssayResultDomainKind extends AssayDomainKind { - public static final String PLATE_COLUMN_NAME = "Plate"; - public static final String WELL_LOCATION_COLUMN_NAME = "WellLocation"; - public static final String WELL_LSID_COLUMN_NAME = "WellLsid"; - public static final String REPLICATE_LSID_COLUMN_NAME = "ReplicateLsid"; - public static final String STATE_COLUMN_NAME = "State"; + public enum Column + { + Plate, + Replicate, + ReplicateLsid, + State, + WellLocation, + WellLsid; + + public FieldKey fieldKey() + { + return FieldKey.fromParts(name()); + } + } public AssayResultDomainKind() { @@ -135,11 +147,11 @@ public Set getMandatoryPropertyNames(Domain domain) { if (provider.isPlateMetadataEnabled(protocol)) { - mandatoryNames.add(PLATE_COLUMN_NAME); - mandatoryNames.add(WELL_LOCATION_COLUMN_NAME); - mandatoryNames.add(WELL_LSID_COLUMN_NAME); - mandatoryNames.add(REPLICATE_LSID_COLUMN_NAME); - mandatoryNames.add(STATE_COLUMN_NAME); + mandatoryNames.add(Column.Plate.name()); + mandatoryNames.add(Column.WellLocation.name()); + mandatoryNames.add(Column.WellLsid.name()); + mandatoryNames.add(Column.ReplicateLsid.name()); + mandatoryNames.add(Column.State.name()); } } } @@ -152,4 +164,23 @@ public boolean allowCalculatedFields() { return true; } + + @Override + public void deletePropertyDescriptor(Domain domain, User user, PropertyDescriptor pd) + { + super.deletePropertyDescriptor(domain, user, pd); + + // SQL Server does not allow for multiple foreign keys to the same table to utilize ON DELETE CASCADE as it may + // cause cycles or multiple cascade paths. The solution is to only ON DELETE CASCADE for one foreign key and + // clean up upon delete of the property for other changes. See the "CREATE TABLE assay.FilterCriteria" + // statement in assay schema upgrade scripts. + if (!OntologyManager.getSqlDialect().isSqlServer()) + return; + + Pair pair = findProviderAndProtocol(domain); + if (pair == null) + return; + + pair.first.removeFilterCriteriaForProperty(pd); + } } diff --git a/assay/api-src/org/labkey/api/assay/AssayRunDomainKind.java b/assay/api-src/org/labkey/api/assay/AssayRunDomainKind.java index 00a6fa11a5f..1af89502bda 100644 --- a/assay/api-src/org/labkey/api/assay/AssayRunDomainKind.java +++ b/assay/api-src/org/labkey/api/assay/AssayRunDomainKind.java @@ -24,10 +24,6 @@ import java.util.Set; -/** - * User: jeckels - * Date: Jan 27, 2012 - */ public class AssayRunDomainKind extends AssayDomainKind { public AssayRunDomainKind() @@ -69,6 +65,7 @@ public Set getMandatoryPropertyNames(Domain domain) if (provider.isPlateMetadataEnabled(protocol)) { mandatoryNames.add(AssayPlateMetadataService.PLATE_SET_COLUMN_NAME); + mandatoryNames.add(AssayPlateMetadataService.HIT_SELECTION_CRITERIA_COLUMN_NAME); } } } diff --git a/assay/package-lock.json b/assay/package-lock.json index da6c80249e9..f728924bd3c 100644 --- a/assay/package-lock.json +++ b/assay/package-lock.json @@ -8,7 +8,7 @@ "name": "assay", "version": "0.0.0", "dependencies": { - "@labkey/components": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", @@ -2211,9 +2211,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.36.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.36.0.tgz", - "integrity": "sha512-cWQd1Umwkg7H/KLWpQ0I3p7GfLHw8kwFVAAtJZDeaykd21lyIypyOgQq+gLvmQJTAi9vRP4eaJ85L+b4o4x9Gw==" + "version": "1.37.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.37.0.tgz", + "integrity": "sha512-PIuzYGEm0O6ydWoXWEqCV+hHGqzDsVZ5Q3JD6i/d/bvp6On0jML9lnmz45hw4ZAXiwLSpd09whaTeSPVxnDxig==" }, "node_modules/@labkey/build": { "version": "8.3.0", @@ -2252,12 +2252,12 @@ } }, "node_modules/@labkey/components": { - "version": "6.6.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.6.0.tgz", - "integrity": "sha512-OVmQjchyUr8VlaKaSDTg+QpGjImEm+dWhnfctvK8uRLb6GynNAkgL7nPv/Bt+awRwtv5L66qXUeSSOcIWqV8sg==", + "version": "6.8.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.8.0.tgz", + "integrity": "sha512-wHErvhP7+d0K6rrXUREgpzj5HjMJTfgQO3iJrg/wW9B9408mjMgtW76MGfjdn1haqepnbEIvUlA7u1Y5qS2X5A==", "dependencies": { "@hello-pangea/dnd": "17.0.0", - "@labkey/api": "1.36.0", + "@labkey/api": "1.37.0", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.0.1", diff --git a/assay/package.json b/assay/package.json index 00e2338ba57..5742df27a46 100644 --- a/assay/package.json +++ b/assay/package.json @@ -12,7 +12,7 @@ "clean": "rimraf resources/web/assay/gen && rimraf resources/views/gen && rimraf resources/web/gen" }, "dependencies": { - "@labkey/components": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", diff --git a/assay/resources/schemas/assay.xml b/assay/resources/schemas/assay.xml index bb1e3fe8897..d01dd0df335 100644 --- a/assay/resources/schemas/assay.xml +++ b/assay/resources/schemas/assay.xml @@ -201,6 +201,17 @@ + + Contains one row per assay result field filter criteria. + + + + + + + + +
Contains one row per well in a plate. diff --git a/assay/resources/schemas/dbscripts/postgresql/assay-24.015-24.016.sql b/assay/resources/schemas/dbscripts/postgresql/assay-24.015-24.016.sql new file mode 100644 index 00000000000..fa1a4ccd8a3 --- /dev/null +++ b/assay/resources/schemas/dbscripts/postgresql/assay-24.015-24.016.sql @@ -0,0 +1,16 @@ +CREATE TABLE assay.FilterCriteria +( + RowId SERIAL, + PropertyId INT NOT NULL, + ReferencePropertyId INT NOT NULL, + DomainId INT NOT NULL, + Operation VARCHAR(50) NOT NULL, + Value VARCHAR(4000) NULL, + + CONSTRAINT PK_FilterCriteria PRIMARY KEY (RowId), + CONSTRAINT FK_FilterCriteria_PropertyDescriptor FOREIGN KEY (PropertyId) REFERENCES exp.PropertyDescriptor (PropertyId) ON DELETE CASCADE, + CONSTRAINT FK_FilterCriteria_PropertyDescriptor_Reference FOREIGN KEY (ReferencePropertyId) REFERENCES exp.PropertyDescriptor (PropertyId) ON DELETE CASCADE, + CONSTRAINT FK_FilterCriteria_DomainDescriptor FOREIGN KEY (DomainId) REFERENCES exp.DomainDescriptor (DomainId) ON DELETE CASCADE +); + +SELECT core.executeJavaUpgradeCode('initializeHitSelectionCriteria'); diff --git a/assay/resources/schemas/dbscripts/sqlserver/assay-24.015-24.016.sql b/assay/resources/schemas/dbscripts/sqlserver/assay-24.015-24.016.sql new file mode 100644 index 00000000000..38564102d01 --- /dev/null +++ b/assay/resources/schemas/dbscripts/sqlserver/assay-24.015-24.016.sql @@ -0,0 +1,20 @@ +CREATE TABLE assay.FilterCriteria +( + RowId INT IDENTITY(1,1), + PropertyId INT NOT NULL, + ReferencePropertyId INT NOT NULL, + DomainId INT NOT NULL, + Operation NVARCHAR(50) NOT NULL, + Value NVARCHAR(4000) NULL, + + CONSTRAINT PK_FilterCriteria PRIMARY KEY (RowId), + CONSTRAINT FK_FilterCriteria_DomainDescriptor FOREIGN KEY (DomainId) REFERENCES exp.DomainDescriptor (DomainId) ON DELETE CASCADE, + + -- SQL Server does not allow for multiple foreign keys to the same table to utilize ON DELETE CASCADE as it may + -- cause cycles or multiple cascade paths. The solution is to only ON DELETE CASCADE for one foreign key and + -- clean up upon delete of the property for other changes. See AssayResultDomainKind.deletePropertyDescriptor(). + CONSTRAINT FK_FilterCriteria_PropertyDescriptor FOREIGN KEY (PropertyId) REFERENCES exp.PropertyDescriptor (PropertyId) ON DELETE CASCADE, + CONSTRAINT FK_FilterCriteria_PropertyDescriptor_Reference FOREIGN KEY (ReferencePropertyId) REFERENCES exp.PropertyDescriptor (PropertyId) ON DELETE NO ACTION +); + +EXEC core.executeJavaUpgradeCode 'initializeHitSelectionCriteria'; diff --git a/assay/src/org/labkey/assay/AssayController.java b/assay/src/org/labkey/assay/AssayController.java index a15e940aee8..26c5d83310d 100644 --- a/assay/src/org/labkey/assay/AssayController.java +++ b/assay/src/org/labkey/assay/AssayController.java @@ -59,6 +59,7 @@ import org.labkey.api.assay.actions.ProtocolIdForm; import org.labkey.api.assay.actions.ReimportRedirectAction; import org.labkey.api.assay.actions.UploadWizardAction; +import org.labkey.api.assay.plate.AssayPlateMetadataService; import org.labkey.api.assay.plate.PlateBasedAssayProvider; import org.labkey.api.assay.sample.AssaySampleLookupContext; import org.labkey.api.assay.security.DesignAssayPermission; @@ -172,6 +173,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -1876,4 +1878,77 @@ public Object execute(SyncRunLineageForm form, BindException errors) throws Exce return success(); } } + + public static class FilterCriteriaColumnsForm + { + private List _columnNames = new ArrayList<>(); + private Integer _protocolId; + + public List getColumnNames() + { + return _columnNames; + } + + public void setColumnNames(List columnNames) + { + _columnNames = columnNames; + } + + public Integer getProtocolId() + { + return _protocolId; + } + + public void setProtocolId(Integer protocolId) + { + _protocolId = protocolId; + } + } + + @Marshal(Marshaller.Jackson) + @RequiresPermission(ReadPermission.class) + public static class FilterCriteriaColumnsAction extends MutatingApiAction + { + private List columnNames; + + @Override + public void validateForm(FilterCriteriaColumnsForm form, Errors errors) + { + if (form.getProtocolId() != null && form.getProtocolId() <= 0) + errors.reject(ERROR_REQUIRED, "A valid \"protocolId\" is required."); + else if (form.getColumnNames() == null || form.getColumnNames().isEmpty()) + errors.reject(ERROR_REQUIRED, "At least one \"columnNames\" must be specified."); + + var columnNameSet = new LinkedHashSet(); + for (String columnName : form.getColumnNames()) + { + var name = StringUtils.trimToNull(columnName); + if (name == null) + { + errors.reject(ERROR_REQUIRED, String.format("A column name of \"%s\" is not supported.", columnName)); + return; + } + + columnNameSet.add(name); + } + + columnNames = new ArrayList<>(columnNameSet); + } + + @Override + public Object execute(FilterCriteriaColumnsForm form, BindException errors) throws Exception + { + if (form.getProtocolId() == null) + return AssayPlateMetadataService.get().previewFilterCriteriaColumns(getContainer(), "FilterCriteriaColumnsAction", columnNames); + + var protocol = ExperimentService.get().getExpProtocol(form.getProtocolId()); + if (protocol == null || !protocol.getContainer().hasPermission(getUser(), ReadPermission.class) || AssayService.get().getProvider(protocol) == null) + { + errors.reject(ERROR_GENERIC, String.format("Unable to resolve assay protocol with id (%d).", form.getProtocolId())); + return null; + } + + return AssayPlateMetadataService.get().previewFilterCriteriaColumns(protocol, form.getColumnNames()); + } + } } diff --git a/assay/src/org/labkey/assay/AssayDomainServiceImpl.java b/assay/src/org/labkey/assay/AssayDomainServiceImpl.java index 1f4f4b1d1f1..10b64e74763 100644 --- a/assay/src/org/labkey/assay/AssayDomainServiceImpl.java +++ b/assay/src/org/labkey/assay/AssayDomainServiceImpl.java @@ -28,6 +28,7 @@ import org.labkey.api.assay.AssayQCService; import org.labkey.api.assay.AssayService; import org.labkey.api.assay.DetectionMethodAssayProvider; +import org.labkey.api.assay.plate.FilterCriteria; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateBasedAssayProvider; import org.labkey.api.assay.plate.PlateService; @@ -45,7 +46,6 @@ import org.labkey.api.exp.api.ExpProtocol; import org.labkey.api.exp.api.ExperimentService; 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.DomainUtil; import org.labkey.api.exp.property.PropertyService; @@ -120,21 +120,28 @@ public GWTProtocol getAssayTemplate(String providerName) { AssayProvider provider = AssayService.get().getProvider(providerName); if (provider == null) - { throw new NotFoundException("Could not find assay provider " + providerName); - } + Pair>>> template = provider.getAssayTemplate(getUser(), getContainer()); return getAssayTemplate(provider, template, false); } - private GWTProtocol getAssayTemplate(AssayProvider provider, Pair>>> template, boolean copy) + private List> getDomains( + AssayProvider provider, + ExpProtocol protocol, + List>> domainInfos, + boolean copy + ) { - ExpProtocol protocol = template.getKey(); List> gwtDomains = new ArrayList<>(); - for (Pair> domainInfo : template.getValue()) + String resultsDomainPrefix = ":" + ExpProtocol.AssayDomainTypes.Result.getPrefix() + "."; + List allFilterCriteria = null; + + for (Pair> domainInfo : domainInfos) { Domain domain = domainInfo.getKey(); GWTDomain gwtDomain = DomainUtil.getDomainDescriptor(getUser(), domain); + boolean isResultsDomain = gwtDomain.getDomainURI().contains(resultsDomainPrefix); // If assay is new default value options and default may not have been available in getDomainDescriptor, so try again with provider. if (provider.allowDefaultValues(domain) && (gwtDomain.getDefaultValueOptions() == null || gwtDomain.getDefaultValueOptions().length == 0)) @@ -143,27 +150,23 @@ private GWTProtocol getAssayTemplate(AssayProvider provider, Pair kind = domain.getDomainKind(); List gwtProps = new ArrayList<>(); - List properties = domain.getProperties(); Map defaultValues = domainInfo.getValue(); - Set mandatoryPropertyDescriptors = new CaseInsensitiveHashSet(kind.getMandatoryPropertyNames(domain)); + Set mandatoryPropertyDescriptors = new CaseInsensitiveHashSet(domain.getDomainKind().getMandatoryPropertyNames(domain)); - for (DomainProperty prop : properties) + for (DomainProperty prop : domain.getProperties()) { GWTPropertyDescriptor gwtProp = getPropertyDescriptor(prop, copy); if (gwtProp.getDefaultValueType() == null) { - // we want to explicitly set these "special" properties NOT to remember the user's last entered + // Explicitly set these "special" properties NOT to remember the user's last entered // value if it hasn't been set before: if (AbstractAssayProvider.PARTICIPANTID_PROPERTY_NAME.equals(prop.getName()) || AbstractAssayProvider.SPECIMENID_PROPERTY_NAME.equals(prop.getName()) || @@ -175,7 +178,7 @@ private GWTProtocol getAssayTemplate(AssayProvider provider, Pair fieldFilterCriteria = allFilterCriteria.stream() + .filter(criterion -> prop.getPropertyId() == criterion.referencePropertyId()) + .toList(); + + gwtProp.setFilterCriteria(FilterCriteria.toGWTFilterCriteria(fieldFilterCriteria)); + } + + gwtProps.add(gwtProp); } + gwtProps.addAll(gwtDomain.getCalculatedFields()); gwtDomain.setFields(gwtProps); gwtDomain.setMandatoryFieldNames(mandatoryPropertyDescriptors); + + if (isResultsDomain) + gwtDomain.setAllowFlagProperties(provider.supportsFlagColumnType(ExpProtocol.AssayDomainTypes.Result)); + + gwtDomains.add(gwtDomain); } + return gwtDomains; + } + + private GWTProtocol getAssayTemplate(AssayProvider provider, Pair>>> template, boolean copy) + { + ExpProtocol protocol = template.getKey(); + GWTProtocol result = new GWTProtocol(); result.setProtocolId(protocol.getRowId() > 0 ? protocol.getRowId() : null); - result.setDomains(gwtDomains); + result.setDomains(getDomains(provider, protocol, template.getValue(), copy)); result.setName(protocol.getName()); result.setProviderName(provider.getName()); result.setDescription(protocol.getDescription()); result.setStatus(protocol.getStatus() != null ? protocol.getStatus().name() : ExpProtocol.Status.Active.name()); - Map gwtProtocolParams = new HashMap<>(); - for (ProtocolParameter property : protocol.getProtocolParameters().values()) + + // Configure protocol parameters { - if (property.getXmlBeanValueType() != SimpleTypeNames.STRING) + Map gwtProtocolParams = new HashMap<>(); + for (ProtocolParameter property : protocol.getProtocolParameters().values()) { - throw new IllegalStateException("Did not expect non-string protocol parameter " + property.getOntologyEntryURI() + " (" + property.getValueType() + ")"); + if (property.getXmlBeanValueType() != SimpleTypeNames.STRING) + throw new IllegalStateException("Did not expect non-string protocol parameter " + property.getOntologyEntryURI() + " (" + property.getValueType() + ")"); + + gwtProtocolParams.put(property.getOntologyEntryURI(), property.getStringValue()); } - gwtProtocolParams.put(property.getOntologyEntryURI(), property.getStringValue()); + result.setProtocolParameters(gwtProtocolParams); } - result.setProtocolParameters(gwtProtocolParams); + if (provider instanceof PlateBasedAssayProvider plateProvider) { Plate plateTemplate = plateProvider.getPlate(getContainer(), protocol); @@ -239,6 +273,7 @@ private GWTProtocol getAssayTemplate(AssayProvider provider, Pair gwtDomain : result.getDomains()) - if (gwtDomain.getDomainURI().contains(":" + ExpProtocol.AssayDomainTypes.Result.getPrefix() + ".")) - gwtDomain.setAllowFlagProperties(supportsFlag); - return result; } @@ -416,23 +445,20 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr else { GWTDomain previous = DomainUtil.getDomainDescriptor(getUser(), domain.getDomainURI(), protocol.getContainer()); - updateDomainDescriptor(domain, protocol, previous, assayProvider, false); + updateDomainDescriptor(assayProvider, protocol, previous, domain, false); domainURIs.add(domain.getDomainURI()); } - } + setPropertyDomainURIs(protocol, domainURIs, assayProvider); } else { protocol = ExperimentService.get().getExpProtocol(assay.getProtocolId().intValue()); - if (protocol == null) - { throw new ValidationException("Assay design has been deleted"); - } - //ensure that the user has edit perms in this container + // ensure that the user has edit perms in this container if (!canUpdateProtocols()) throw new ValidationException("You do not have sufficient permissions to update this Assay"); @@ -488,11 +514,10 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr if (provider instanceof PlateBasedAssayProvider plateProvider && assay.getSelectedPlateTemplate() != null) { Plate plate = PlateManager.get().getPlateByName(getContainer(), assay.getSelectedPlateTemplate()); - if (plate != null) - plateProvider.setPlate(getContainer(), protocol, plate); - else + if (plate == null) throw new ValidationException("The selected plate could not be found. Perhaps it was deleted by another user?"); + plateProvider.setPlate(getContainer(), protocol, plate); String selectedFormat = assay.getSelectedMetadataInputFormat(); SampleMetadataInputFormat inputFormat = SampleMetadataInputFormat.valueOf(selectedFormat); if (inputFormat != null) @@ -500,25 +525,24 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr } // data transform scripts - List transformScripts = new ArrayList<>(); List submittedScripts = assay.getProtocolTransformScripts(); if (!submittedScripts.isEmpty() && !canUpdateTransformationScript()) throw new ValidationException("You must be a platform developer or site admin to configure assay transformation scripts."); - for (String script : assay.getProtocolTransformScripts()) + + List transformScripts = new ArrayList<>(); + for (String script : submittedScripts) { if (!StringUtils.isBlank(script)) - { transformScripts.add(new File(script)); - } } if (provider instanceof DetectionMethodAssayProvider dmProvider && assay.getSelectedDetectionMethod() != null) { String detectionMethod = assay.getSelectedDetectionMethod(); - if (detectionMethod != null) - dmProvider.setSelectedDetectionMethod(getContainer(), protocol, detectionMethod); - else + if (detectionMethod == null) throw new ValidationException("The selected detection method could not be found."); + + dmProvider.setSelectedDetectionMethod(getContainer(), protocol, detectionMethod); } ValidationException scriptValidation = provider.setValidationAndAnalysisScripts(protocol, transformScripts); @@ -553,23 +577,15 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr } if (autoLinkTargetContainerId != null) - { props.put(StudyPublishService.AUTO_LINK_TARGET_PROPERTY_URI, new ObjectProperty(protocol.getLSID(), protocol.getContainer(), StudyPublishService.AUTO_LINK_TARGET_PROPERTY_URI, autoLinkTargetContainerId)); - } else - { props.remove(StudyPublishService.AUTO_LINK_TARGET_PROPERTY_URI); - } String autoLinkCategory = assay.getAutoLinkCategory(); if (autoLinkCategory != null) - { props.put(StudyPublishService.AUTO_LINK_CATEGORY_PROPERTY_URI, new ObjectProperty(protocol.getLSID(), protocol.getContainer(), StudyPublishService.AUTO_LINK_CATEGORY_PROPERTY_URI, autoLinkCategory)); - } else - { props.remove(StudyPublishService.AUTO_LINK_CATEGORY_PROPERTY_URI); - } protocol.setObjectProperties(props); @@ -578,7 +594,7 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr for (GWTDomain domain : assay.getDomains()) { GWTDomain previous = DomainUtil.getDomainDescriptor(getUser(), domain.getDomainURI(), protocol.getContainer()); - updateDomainDescriptor(domain, protocol, previous, provider, hasNameChange); + updateDomainDescriptor(provider, protocol, previous, domain, hasNameChange); boolean hasExistingCalcFields = previous != null && !previous.getCalculatedFields().isEmpty(); GWTDomain savedDomain = DomainUtil.getDomainDescriptor(getUser(), domain.getDomainURI(), protocol.getContainer()); @@ -606,31 +622,33 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr } private void updateDomainDescriptor( - GWTDomain domain, - ExpProtocol protocol, - GWTDomain previous, AssayProvider provider, + ExpProtocol protocol, + GWTDomain original, + GWTDomain update, boolean hasNameChange ) throws ValidationException { - for (GWTPropertyDescriptor prop : domain.getFields()) + for (GWTPropertyDescriptor prop : update.getFields()) { if (prop.getLookupQuery() != null) - { prop.setLookupQuery(prop.getLookupQuery().replace(AbstractAssayProvider.ASSAY_NAME_SUBSTITUTION, protocol.getName())); - } } - provider.changeDomain(getUser(), protocol, previous, domain); String auditComment = null; if (hasNameChange) - { - auditComment = "The name of the assay domain '" + previous.getName() + "' was changed to '" + domain.getName() + "'."; - } + auditComment = "The name of the assay domain '" + original.getName() + "' was changed to '" + update.getName() + "'."; + + // Before update + provider.beforeDomainChange(getUser(), protocol, original, update); + + // Update + ValidationException validationErrors = DomainUtil.updateDomainDescriptor(original, update, getContainer(), getUser(), hasNameChange, auditComment); + if (validationErrors.hasErrors()) + throw validationErrors; - ValidationException domainErrors = DomainUtil.updateDomainDescriptor(previous, domain, getContainer(), getUser(), hasNameChange, auditComment); - if (domainErrors.hasErrors()) - throw domainErrors; + // After update + provider.afterDomainChange(getUser(), protocol, original, update); } private boolean canUpdateProtocols() diff --git a/assay/src/org/labkey/assay/AssayManager.java b/assay/src/org/labkey/assay/AssayManager.java index 89a061e12a9..1a10aa418af 100644 --- a/assay/src/org/labkey/assay/AssayManager.java +++ b/assay/src/org/labkey/assay/AssayManager.java @@ -429,7 +429,7 @@ public void onBeforeAssayResultDelete(Container container, User user, ExpRun run } @Override - public WebPartView createAssayListView(ViewContext context, boolean portalView, BindException errors) + public WebPartView createAssayListView(ViewContext context, boolean portalView, BindException errors) { String name = AssaySchema.ASSAY_LIST_TABLE_NAME; UserSchema schema = createSchema(context.getUser(), context.getContainer(), null); diff --git a/assay/src/org/labkey/assay/AssayModule.java b/assay/src/org/labkey/assay/AssayModule.java index ded8dbba4d6..2d378547c65 100644 --- a/assay/src/org/labkey/assay/AssayModule.java +++ b/assay/src/org/labkey/assay/AssayModule.java @@ -118,7 +118,7 @@ public String getName() @Override public Double getSchemaVersion() { - return 24.015; + return 24.016; } @Override diff --git a/assay/src/org/labkey/assay/AssayUpgradeCode.java b/assay/src/org/labkey/assay/AssayUpgradeCode.java index bc360e2102a..62fed385ea1 100644 --- a/assay/src/org/labkey/assay/AssayUpgradeCode.java +++ b/assay/src/org/labkey/assay/AssayUpgradeCode.java @@ -74,6 +74,7 @@ import java.util.Map; import java.util.Set; +import static org.labkey.api.assay.plate.AssayPlateMetadataService.HIT_SELECTION_CRITERIA_COLUMN_NAME; import static org.labkey.api.data.Table.CREATED_BY_COLUMN_NAME; import static org.labkey.api.data.Table.CREATED_COLUMN_NAME; import static org.labkey.api.data.Table.MODIFIED_BY_COLUMN_NAME; @@ -830,10 +831,10 @@ public static void initializeWellExclusions(ModuleContext ctx) throws Exception { // ensure the QC state column exists in the result domain Domain resultDomain = provider.getResultsDomain(protocol); - if (resultDomain.getPropertyByName(AssayResultDomainKind.STATE_COLUMN_NAME) == null) + if (resultDomain.getPropertyByName(AssayResultDomainKind.Column.State.name()) == null) { - _log.info(String.format("Adding the %s field to the results domain for assay : %s", AssayResultDomainKind.STATE_COLUMN_NAME, protocol.getName())); - DomainProperty dp = resultDomain.addProperty(new PropertyStorageSpec(AssayResultDomainKind.STATE_COLUMN_NAME, JdbcType.INTEGER)); + _log.info(String.format("Adding the %s field to the results domain for assay : %s", AssayResultDomainKind.Column.State.name(), protocol.getName())); + DomainProperty dp = resultDomain.addProperty(new PropertyStorageSpec(AssayResultDomainKind.Column.State.name(), JdbcType.INTEGER)); dp.setLabel("QC State"); dp.setImportAliasSet(Set.of("QCState", "QC State")); dp.setLookup(new Lookup(null, SchemaKey.fromParts(CoreSchema.getInstance().getSchemaName()), CoreSchema.DATA_STATES_TABLE_NAME)); @@ -848,4 +849,46 @@ public static void initializeWellExclusions(ModuleContext ctx) throws Exception tx.commit(); } } + + /** + * Called from assay-24.015-24.016.sql, in order to support hit selection criteria for plate enabled assays. + * The upgrade creates the run domain hit selection criteria field. + */ + @DeferredUpgrade + public static void initializeHitSelectionCriteria(ModuleContext ctx) throws Exception + { + if (ctx.isNewInstall()) + return; + + try (DbScope.Transaction tx = AssayDbSchema.getInstance().getSchema().getScope().ensureTransaction()) + { + Set protocols = new HashSet<>(); + for (Container container : ContainerManager.getAllChildren(ContainerManager.getRoot())) + { + if (isBiologicsFolder(container)) + protocols.addAll(AssayService.get().getAssayProtocols(container)); + } + + for (ExpProtocol protocol : protocols) + { + AssayProvider provider = AssayService.get().getProvider(protocol); + if (provider != null && provider.isPlateMetadataEnabled(protocol)) + { + // ensure the QC state column exists in the result domain + Domain runDomain = provider.getRunDomain(protocol); + if (runDomain != null && runDomain.getPropertyByName(HIT_SELECTION_CRITERIA_COLUMN_NAME) == null) + { + _log.info("Adding the \"{}\" field to the run domain for assay : {}", HIT_SELECTION_CRITERIA_COLUMN_NAME, protocol.getName()); + DomainProperty dp = runDomain.addProperty(new PropertyStorageSpec(HIT_SELECTION_CRITERIA_COLUMN_NAME, JdbcType.VARCHAR)); + dp.setShownInInsertView(false); + dp.setShownInUpdateView(false); + + runDomain.save(User.getAdminServiceUser()); + } + } + } + + tx.commit(); + } + } } diff --git a/assay/src/org/labkey/assay/TSVProtocolSchema.java b/assay/src/org/labkey/assay/TSVProtocolSchema.java index 9e1b2c1fbb0..096414d2270 100644 --- a/assay/src/org/labkey/assay/TSVProtocolSchema.java +++ b/assay/src/org/labkey/assay/TSVProtocolSchema.java @@ -149,14 +149,15 @@ private class _AssayResultTable extends AssayResultTable } } - List defaultColumns = new ArrayList<>(getDefaultVisibleColumns()); if (getProvider().isPlateMetadataEnabled(getProtocol())) { + List defaultColumns = new ArrayList<>(getDefaultVisibleColumns()); + // plate related triggers - addTriggerFactory(new AssayPlateTriggerFactory(getProtocol())); + addTriggerFactory(new AssayPlateTriggerFactory(getProvider(), getProtocol())); // join to the well table which may have plate metadata - ColumnInfo wellLsidCol = getColumn(AssayResultDomainKind.WELL_LSID_COLUMN_NAME); + ColumnInfo wellLsidCol = getColumn(AssayResultDomainKind.Column.WellLsid.name()); if (wellLsidCol != null) { BaseColumnInfo col = new AliasedColumn("Well", wellLsidCol); @@ -193,10 +194,10 @@ private class _AssayResultTable extends AssayResultTable Domain replicateDomain = AssayPlateMetadataService.get().getPlateReplicateStatsDomain(getProtocol()); if (replicateDomain != null) { - ColumnInfo replicateLsidCol = getColumn(AssayResultDomainKind.REPLICATE_LSID_COLUMN_NAME); + ColumnInfo replicateLsidCol = getColumn(AssayResultDomainKind.Column.ReplicateLsid.name()); if (replicateLsidCol != null) { - BaseColumnInfo replicateCol = new AliasedColumn("Replicate", replicateLsidCol); + BaseColumnInfo replicateCol = new AliasedColumn(AssayResultDomainKind.Column.Replicate.name(), replicateLsidCol); replicateCol.setFk(QueryForeignKey .from(getUserSchema(), getContainerFilter()) .to(PLATE_REPLICATE_STATS_TABLE, PlateReplicateStatsDomainKind.Column.Lsid.name(), null) @@ -208,7 +209,7 @@ private class _AssayResultTable extends AssayResultTable // adjust the default columns to position the replicate columns adjacent to the measures they track Map replicateFields = new HashMap<>(); for (DomainProperty prop : replicateDomain.getProperties()) - replicateFields.put(prop.getName(), FieldKey.fromParts("Replicate", prop.getName())); + replicateFields.put(prop.getName(), FieldKey.fromParts(AssayResultDomainKind.Column.Replicate.name(), prop.getName())); List newDefaultColumns = new ArrayList<>(); for (FieldKey fk : defaultColumns) @@ -221,6 +222,7 @@ private class _AssayResultTable extends AssayResultTable defaultColumns = newDefaultColumns; } } + setDefaultVisibleColumns(defaultColumns); } } diff --git a/assay/src/org/labkey/assay/TsvAssayProvider.java b/assay/src/org/labkey/assay/TsvAssayProvider.java index a466b04fbfa..21618259292 100644 --- a/assay/src/org/labkey/assay/TsvAssayProvider.java +++ b/assay/src/org/labkey/assay/TsvAssayProvider.java @@ -18,6 +18,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jmock.Expectations; @@ -41,17 +42,24 @@ import org.labkey.api.assay.TsvDataHandler; import org.labkey.api.assay.actions.AssayRunUploadForm; import org.labkey.api.assay.plate.AssayPlateMetadataService; +import org.labkey.api.assay.plate.FilterCriteria; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DbSequence; import org.labkey.api.data.DbSequenceManager; +import org.labkey.api.data.Results; +import org.labkey.api.data.RuntimeSQLException; +import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; +import org.labkey.api.data.Sort; +import org.labkey.api.data.SqlExecutor; +import org.labkey.api.data.Table; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; -import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.Lsid; import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.PropertyType; import org.labkey.api.exp.XarContext; import org.labkey.api.exp.api.ExpData; @@ -61,8 +69,10 @@ import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.property.Domain; import org.labkey.api.exp.property.DomainProperty; +import org.labkey.api.exp.property.DomainUtil; import org.labkey.api.exp.property.PropertyService; import org.labkey.api.gwt.client.model.GWTDomain; +import org.labkey.api.gwt.client.model.GWTFilterCriteria; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; @@ -71,6 +81,7 @@ import org.labkey.api.qc.DataExchangeHandler; import org.labkey.api.qc.TsvDataExchangeHandler; import org.labkey.api.query.FieldKey; +import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; import org.labkey.api.study.assay.ParticipantVisitResolverType; @@ -78,19 +89,24 @@ import org.labkey.api.study.assay.ThawListResolverType; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Pair; -import org.labkey.api.util.UnexpectedException; +import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.HttpView; import org.labkey.api.view.JspView; import org.labkey.assay.plate.query.PlateSchema; import org.labkey.assay.plate.query.PlateSetTable; import org.labkey.assay.plate.query.PlateTable; +import org.labkey.assay.query.AssayDbSchema; import java.io.File; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -98,13 +114,10 @@ import static org.labkey.api.data.CompareType.STARTS_WITH; -/** - * User: brittp - * Date: Jul 11, 2007 - * Time: 9:59:39 AM - */ public class TsvAssayProvider extends AbstractTsvAssayProvider { + private static final Logger LOG = LogHelper.getLogger(TsvAssayProvider.class, "General Assay Provider"); + public static final String NAME = "General"; public static final String PLATE_TEMPLATE_PROPERTY_NAME = "PlateTemplate"; public static final String PLATE_TEMPLATE_PROPERTY_CAPTION = "Plate Template"; @@ -375,7 +388,7 @@ public boolean isExclusionSupported() @Override public boolean supportsFlagColumnType(ExpProtocol.AssayDomainTypes type) { - return type== ExpProtocol.AssayDomainTypes.Result; + return ExpProtocol.AssayDomainTypes.Result.equals(type); } @Override @@ -393,9 +406,9 @@ private boolean hasDomainNameChanged(ExpProtocol protocol, GWTDomain orig, GWTDomain update) + public void beforeDomainChange(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException { - super.changeDomain(user, protocol, orig, update); + super.beforeDomainChange(user, protocol, orig, update); if (hasDomainNameChanged(protocol, orig)) { @@ -425,6 +438,15 @@ public void changeDomain(User user, ExpProtocol protocol, GWTDomain newFields = new ArrayList<>(); - if (!existingFields.contains(AssayResultDomainKind.PLATE_COLUMN_NAME)) + if (!existingFields.contains(AssayResultDomainKind.Column.Plate.name())) { - GWTPropertyDescriptor plate = new GWTPropertyDescriptor(AssayResultDomainKind.PLATE_COLUMN_NAME, PropertyType.INTEGER.getTypeUri()); + GWTPropertyDescriptor plate = new GWTPropertyDescriptor(AssayResultDomainKind.Column.Plate.name(), PropertyType.INTEGER.getTypeUri()); plate.setLookupSchema(PlateSchema.SCHEMA_NAME); plate.setLookupQuery(PlateTable.NAME); plate.setLookupContainer(null); @@ -451,18 +472,18 @@ public void changeDomain(User user, ExpProtocol protocol, GWTDomain orig, GWTDomain update) throws ValidationException + { + super.afterDomainChange(user, protocol, orig, update); + + if (isResultsDomain(update)) + { + if (isPlateMetadataEnabled(protocol)) + { + // Update fields in the replicate stats table to match any changes to measures in the results domain + AssayPlateMetadataService.get().updateReplicateStatsDomain(user, protocol, orig, update); } + + // Filter criteria are only available to result domains + updateFilterCriteria(user, protocol, orig, update); } } @@ -545,6 +574,206 @@ public boolean supportsSampleLookupsAsMaterialInputs() return true; } + @Override + protected @NotNull List getFilterCriteria(ExpProtocol protocol, Domain domain) + { + return new ArrayList<>(getFilterCriteriaMap(protocol, domain).values()); + } + + private @NotNull Map getFilterCriteriaMap(ExpProtocol protocol, Domain domain) + { + var criteria = new LinkedHashMap(); + var filter = new SimpleFilter(FieldKey.fromParts("DomainId"), domain.getTypeId()); + + Domain replicateStatsDomain = null; + boolean isReplicateStatsResolved = false; + + try (Results results = new TableSelector(AssayDbSchema.getInstance().getTableInfoFilterCriteria(), filter, new Sort(FieldKey.fromParts("RowId"))).getResults()) + { + while (results.next()) + { + var propertyId = results.getInt("PropertyId"); + var rowId = results.getInt("RowId"); + + var property = domain.getProperty(propertyId); + + // Lookup on the replicate stats domain + if (property == null) + { + if (!isReplicateStatsResolved) + { + isReplicateStatsResolved = true; + replicateStatsDomain = AssayPlateMetadataService.get().getPlateReplicateStatsDomain(protocol); + } + + if (replicateStatsDomain != null) + property = replicateStatsDomain.getProperty(propertyId); + } + + if (property == null) + { + LOG.warn("Failed to resolve filter criteria property for propertyId ({}). See rowId ({}).", propertyId, rowId); + continue; + } + + var criterion = new FilterCriteria( + results.getString("Operation"), + results.getString("Value"), + property.getPropertyId(), + property.getName(), + results.getInt("ReferencePropertyId"), + results.getInt("DomainId") + ); + + criteria.put(rowId, criterion); + } + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + + return criteria; + } + + @Override + protected boolean hasFilterCriteria(ExpProtocol protocol, Domain domain) + { + var filter = new SimpleFilter(FieldKey.fromParts("DomainId"), domain.getTypeId()); + return new TableSelector(AssayDbSchema.getInstance().getTableInfoFilterCriteria(), Collections.singleton("RowId"), filter, null).exists(); + } + + private @NotNull GWTDomain getSavedDomain(User user, ExpProtocol protocol, GWTDomain update) throws ValidationException + { + GWTDomain savedDomain = DomainUtil.getDomainDescriptor(user, update.getDomainURI(), protocol.getContainer()); + if (savedDomain == null) + throw new ValidationException(String.format("Failed to resolve saved domain for domain URI \"%s\" in %s.", update.getDomainURI(), protocol.getContainer().getPath())); + + return savedDomain; + } + + private void updateFilterCriteria( + User user, + ExpProtocol protocol, + GWTDomain original, + GWTDomain update + ) throws ValidationException + { + assert AssayDbSchema.getInstance().getSchema().getScope().isTransactionActive(); + + Domain replicateStatsDomain = AssayPlateMetadataService.get().getPlateReplicateStatsDomain(protocol); + GWTDomain savedDomain = null; + + Set newCriteria = new LinkedHashSet<>(); + for (GWTPropertyDescriptor prop : update.getFields()) + { + List filterCriteria = prop.getFilterCriteria(); + if (filterCriteria == null || filterCriteria.isEmpty()) + continue; + + int referencePropertyId = prop.getPropertyId(); + if (referencePropertyId <= 0) + { + if (savedDomain == null) + savedDomain = getSavedDomain(user, protocol, update); + + for (GWTPropertyDescriptor savedProp : savedDomain.getFields(true)) + { + if (savedProp.getPropertyURI().equals(prop.getPropertyURI())) + { + referencePropertyId = savedProp.getPropertyId(); + break; + } + } + + if (referencePropertyId <= 0) + throw new ValidationException(String.format("Failed to resolve \"referencePropertyId\" for field \"%s\"", prop.getName())); + } + + int domainId = update.getDomainId(); + if (domainId == 0) + { + if (savedDomain == null) + savedDomain = getSavedDomain(user, protocol, update); + + domainId = savedDomain.getDomainId(); + } + + newCriteria.addAll(FilterCriteria.fromGWTFilterCriteria(filterCriteria, referencePropertyId, prop.getName(), domainId, replicateStatsDomain)); + } + + Map keyedCriteria = getFilterCriteriaMap(protocol, getResultsDomain(protocol)); + Set oldCriteria = new HashSet<>(keyedCriteria.values()); + Set toAdd = new LinkedHashSet<>(newCriteria); + Set toRemove = new HashSet<>(); + + for (var criterion : newCriteria) + { + if (oldCriteria.contains(criterion)) + toAdd.remove(criterion); + } + + for (var criterion : oldCriteria) + { + if (!newCriteria.contains(criterion)) + { + for (var entry : keyedCriteria.entrySet()) + { + if (entry.getValue().equals(criterion)) + toRemove.add(entry.getKey()); + } + } + } + + if (toAdd.isEmpty() && toRemove.isEmpty()) + return; + + var table = AssayDbSchema.getInstance().getTableInfoFilterCriteria(); + + if (!toRemove.isEmpty()) + { + var sql = new SQLFragment("DELETE FROM ").append(table) + .append(" WHERE RowId ").appendInClause(toRemove, table.getSqlDialect()); + new SqlExecutor(table.getSchema()).execute(sql); + } + + if (!toAdd.isEmpty()) + { + List> criteriaToInsert = new ArrayList<>(); + for (var criterion : toAdd) + criteriaToInsert.add(Arrays.asList(criterion.propertyId(), criterion.referencePropertyId(), criterion.domainId(), criterion.operation(), criterion.value())); + + String sql = "INSERT INTO " + table + " (propertyId, referencePropertyId, domainId, operation, value) VALUES (?, ?, ?, ?, ?)"; + + try + { + Table.batchExecute(table.getSchema(), sql, criteriaToInsert); + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + } + } + + @Override + public void removeFilterCriteriaForProperty(PropertyDescriptor pd) + { + assert AssayDbSchema.getInstance().getSchema().getScope().isTransactionActive(); + + var table = AssayDbSchema.getInstance().getTableInfoFilterCriteria(); + var sql = new SQLFragment("DELETE FROM ").append(table) + .append(" WHERE (PropertyId = ? OR ReferencePropertyId = ?)") + .addAll(pd.getPropertyId(), pd.getPropertyId()); + + new SqlExecutor(table.getSchema()).execute(sql); + } + + private static boolean isResultsDomain(GWTDomain domain) + { + return domain != null && domain.getDomainURI().contains(":" + ExpProtocol.AssayDomainTypes.Result.getPrefix() + "."); + } + public static class TestCase extends Assert { private Mockery _context; @@ -671,7 +900,7 @@ public void testPipelineDataCollectorList() @Test public void testReshowDataCollectorList() { - // Simulate an error reshow, where the user should be able to reuse the existing file or upload a replacment + // Simulate an error reshow, where the user should be able to reuse the existing file or upload a replacement _context.checking(new Expectations(){{ allowing(_session).getAttribute(PipelineDataCollector.class.getName()); will(returnValue(new HashMap())); diff --git a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java index 6a5e222cd47..77ab3f7533b 100644 --- a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java @@ -35,6 +35,7 @@ import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.collections.CaseInsensitiveMapWrapper; import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.DbScope; @@ -43,6 +44,7 @@ import org.labkey.api.data.ParameterMapStatement; import org.labkey.api.data.PropertyStorageSpec; import org.labkey.api.data.Results; +import org.labkey.api.data.RuntimeSQLException; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.Sort; @@ -66,8 +68,8 @@ import org.labkey.api.exp.api.ExpRun; import org.labkey.api.exp.api.ExperimentService; 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.DomainUtil; import org.labkey.api.exp.property.PropertyService; import org.labkey.api.gwt.client.model.GWTDomain; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; @@ -87,6 +89,7 @@ import org.labkey.api.util.TestContext; import org.labkey.api.util.UnexpectedException; import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.ActionURL; import org.labkey.assay.TSVProtocolSchema; import org.labkey.assay.plate.model.WellBean; import org.labkey.assay.plate.query.PlateTable; @@ -95,6 +98,7 @@ import org.labkey.vfs.FileLike; import java.io.IOException; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -110,8 +114,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.labkey.api.assay.AssayResultDomainKind.REPLICATE_LSID_COLUMN_NAME; -import static org.labkey.api.assay.AssayResultDomainKind.WELL_LSID_COLUMN_NAME; +import static org.labkey.api.assay.AssayRunUploadContext.ReImportOption.MERGE_DATA; public class AssayPlateMetadataServiceImpl implements AssayPlateMetadataService { @@ -128,8 +131,8 @@ public DataIteratorBuilder mergePlateMetadata( ) { Domain resultDomain = provider.getResultsDomain(protocol); - DomainProperty plateProperty = resultDomain.getPropertyByName(AssayResultDomainKind.PLATE_COLUMN_NAME); - DomainProperty wellLocationProperty = resultDomain.getPropertyByName(AssayResultDomainKind.WELL_LOCATION_COLUMN_NAME); + DomainProperty plateProperty = resultDomain.getPropertyByName(AssayResultDomainKind.Column.Plate.name()); + DomainProperty wellLocationProperty = resultDomain.getPropertyByName(AssayResultDomainKind.Column.WellLocation.name()); return DataIteratorUtil.mapTransformer(rows, cols -> { @@ -202,22 +205,20 @@ public Map apply(Map row) // need to adjust the column value to be 0 based to match the template locations well.setColumn(well.getColumn() - 1); - if (positionToWell.containsKey(well)) - { - WellBean wellBean = positionToWell.get(well); - for (WellCustomField customField : PlateManager.get().getWellCustomFields(user, plate, wellBean.getRowId())) - row.put(customField.getName(), customField.getValue()); + if (!positionToWell.containsKey(well)) + throw new RuntimeValidationException("Unable to resolve well \"" + wellLocation + "\" for plate \"" + plate.getName() + "\"."); - // include the sample information from the well (Issue 50276) - if (!sampleMap.isEmpty()) - { - ExpMaterial sample = sampleMap.get(wellBean.getSampleId()); - row.put("SampleID", sample != null ? sample.getRowId() : null); - row.put("SampleName", sample != null ? sample.getName() : null); - } + WellBean wellBean = positionToWell.get(well); + for (WellCustomField customField : PlateManager.get().getWellCustomFields(user, plate, wellBean.getRowId())) + row.put(customField.getName(), customField.getValue()); + + // Issue 50276: include the sample information from the well + if (!sampleMap.isEmpty()) + { + ExpMaterial sample = sampleMap.get(wellBean.getSampleId()); + row.put("SampleID", sample != null ? sample.getRowId() : null); + row.put("SampleName", sample != null ? sample.getName() : null); } - else - throw new RuntimeValidationException("Unable to resolve well \"" + wellLocation + "\" for plate \"" + plate.getName() + "\"."); return row; } @@ -254,7 +255,7 @@ public DataIteratorBuilder parsePlateData( if (context.getReRunId() != null) { // check if we are merging the re-imported data - if (context.getReImportOption() == AssayRunUploadContext.ReImportOption.MERGE_DATA) + if (context.getReImportOption() == MERGE_DATA) rows = mergeReRunData(container, user, context, rows, plates, provider, protocol, data, dataFile); else { @@ -348,7 +349,7 @@ private List> mergeReRunData( Set incomingPlates = new HashSet<>(); // incoming plates may be either row IDs or plate IDs for (var row : rows) { - var plateId = row.get(AssayResultDomainKind.PLATE_COLUMN_NAME); + var plateId = row.get(AssayResultDomainKind.Column.Plate.name()); if (plateId != null) incomingPlates.add(plateId); } @@ -360,20 +361,20 @@ private List> mergeReRunData( throw new ExperimentException(String.format("Unable to query the assay results for protocol : %s", protocol.getName())); // The plate identifier is either a row ID or plate ID on incoming data, need to match that when merging existing data. - FieldKey plateFieldKey = FieldKey.fromParts(AssayResultDomainKind.PLATE_COLUMN_NAME); + FieldKey plateFieldKey = FieldKey.fromParts(AssayResultDomainKind.Column.Plate.name()); // Note that in the case where there is a transform script on the assay design, the LK data parsing might not have // found any rows and we might be deferring to the transform script to do that parsing. This block of code should // be able to proceed in that case by just passing through all run results to the transform script for the run being replaced. if (!rows.isEmpty()) { - Object plateObj = rows.get(0).get(AssayResultDomainKind.PLATE_COLUMN_NAME); + Object plateObj = rows.get(0).get(AssayResultDomainKind.Column.Plate.name()); if (plateObj instanceof String) - plateFieldKey = FieldKey.fromParts(AssayResultDomainKind.PLATE_COLUMN_NAME, PlateTable.Column.PlateId.name()); + plateFieldKey = FieldKey.fromParts(AssayResultDomainKind.Column.Plate.name(), PlateTable.Column.PlateId.name()); } FieldKey finalPlateFieldKey = plateFieldKey; List columns = resultsTable.getDomain().getProperties().stream().map(dp -> { - if (dp.getName().equalsIgnoreCase(AssayResultDomainKind.PLATE_COLUMN_NAME)) + if (dp.getName().equalsIgnoreCase(AssayResultDomainKind.Column.Plate.name())) return finalPlateFieldKey; return FieldKey.fromParts(dp.getName()); }).toList(); @@ -399,7 +400,7 @@ private List> mergeReRunData( if (rowMap.containsKey(entry.getKey())) row.put(entry.getValue().getName(), rowMap.get(entry.getKey())); } - row.put(AssayResultDomainKind.PLATE_COLUMN_NAME, plate); + row.put(AssayResultDomainKind.Column.Plate.name(), plate); newRows.add(row); prevPlateRowIDs.add(plateMap.get(plate).getRowId()); } @@ -498,7 +499,7 @@ private boolean isGridFormat(List> data) return true; // only the tabular formats will have the well location field - return !data.get(0).containsKey(AssayResultDomainKind.WELL_LOCATION_COLUMN_NAME) && !data.get(0).containsKey("Well Location"); + return !data.get(0).containsKey(AssayResultDomainKind.Column.WellLocation.name()) && !data.get(0).containsKey("Well Location"); } private List> parsePlateRows( @@ -508,9 +509,9 @@ private List> parsePlateRows( List> data ) throws ExperimentException { - DomainProperty plateProp = provider.getResultsDomain(protocol).getPropertyByName(AssayResultDomainKind.PLATE_COLUMN_NAME); + DomainProperty plateProp = provider.getResultsDomain(protocol).getPropertyByName(AssayResultDomainKind.Column.Plate.name()); Set importAliases = new CaseInsensitiveHashSet(plateProp.getImportAliasSet()); - importAliases.add(AssayResultDomainKind.PLATE_COLUMN_NAME); + importAliases.add(AssayResultDomainKind.Column.Plate.name()); // check whether the data rows have plate identifiers String plateIdField = data.get(0).keySet().stream().filter(importAliases::contains).findFirst().orElse(null); @@ -542,7 +543,7 @@ private List> parsePlateRows( for (Map row : data) { // well location field is required, return if not provided or it will fail downstream - String well = String.valueOf(row.get(AssayResultDomainKind.WELL_LOCATION_COLUMN_NAME)); + String well = String.valueOf(row.get(AssayResultDomainKind.Column.WellLocation.name())); if (well == null) return data; @@ -552,7 +553,7 @@ private List> parsePlateRows( positions.add(position); Map newRow = new HashMap<>(row); - newRow.put(AssayResultDomainKind.PLATE_COLUMN_NAME, plates.get(curPlate).getRowId()); + newRow.put(AssayResultDomainKind.Column.Plate.name(), plates.get(curPlate).getRowId()); newData.add(newRow); if (++rowCount >= plateSize) @@ -593,7 +594,7 @@ else if (k instanceof String s && (plate.getPlateId().equalsIgnoreCase(s) || pla }); if (plateRowId != null) - newRow.put(AssayResultDomainKind.PLATE_COLUMN_NAME, plateRowId); + newRow.put(AssayResultDomainKind.Column.Plate.name(), plateRowId); } newData.add(newRow); @@ -830,8 +831,8 @@ else if (plateTypeGrids.keySet().size() > 1) private Map getDataRowFromWell(String plateId, Well well, String measure) { Map row = new CaseInsensitiveHashMap<>(); - row.put(AssayResultDomainKind.PLATE_COLUMN_NAME, plateId); - row.put(AssayResultDomainKind.WELL_LOCATION_COLUMN_NAME, well.getDescription()); + row.put(AssayResultDomainKind.Column.Plate.name(), plateId); + row.put(AssayResultDomainKind.Column.WellLocation.name(), well.getDescription()); row.put(measure, getWellValue(well)); return row; } @@ -858,11 +859,20 @@ public OntologyManager.UpdateableTableImportHelper getImportHelper( return new PlateMetadataImportHelper(data, container, user, run, protocol, provider, context); } - @Override - public void updateReplicateStatsDomain(User user, ExpProtocol protocol, GWTDomain update, Domain resultsDomain) throws ExperimentException + private @NotNull DomainProperty addField(Domain replicateDomain, String fieldName) + { + // create the property and copy the format + PropertyStorageSpec spec = new PropertyStorageSpec(fieldName, JdbcType.DOUBLE); + + // Default formatting is 4 decimal places + DomainProperty domainProperty = replicateDomain.addProperty(spec); + domainProperty.setFormat("#.####"); + + return domainProperty; + } + + private Map getExistingFields(Domain replicateDomain) { - Domain replicateDomain = ensurePlateReplicateStatsDomain(protocol); - boolean domainDirty = false; Set domainBaseProperties = replicateDomain.getBaseProperties().stream().map(DomainProperty::getName).collect(Collectors.toSet()); Map existingFields = new HashMap<>(); replicateDomain.getProperties().forEach(dp -> { @@ -870,65 +880,206 @@ public void updateReplicateStatsDomain(User user, ExpProtocol protocol, GWTDomai existingFields.put(dp.getName(), dp); }); - for (GWTPropertyDescriptor prop : update.getFields()) + return existingFields; + } + + @Override + public Map> previewFilterCriteriaColumns(@NotNull ExpProtocol protocol, List columnNames) + { + return previewFilterCriteriaColumns(protocol.getContainer(), protocol.getName(), columnNames); + } + + @Override + public Map> previewFilterCriteriaColumns(@NotNull Container container, String protocolName, List columnNames) + { + if (columnNames.isEmpty()) + return Collections.emptyMap(); + + var replicateDomain = ensurePlateReplicateStatsDomain(container, protocolName); + var existingFields = getExistingFields(replicateDomain); + var columnMap = new HashMap>(); + + for (var rawName : columnNames) + { + var columnName = StringUtils.trimToNull(rawName); + if (columnName == null) + continue; + + var properties = new ArrayList(); + + for (var name : PlateReplicateStatsDomainKind.getStatsFieldNames(columnName)) + { + DomainProperty dp; + if (existingFields.containsKey(name)) + dp = existingFields.get(name); + else + dp = addField(replicateDomain, name); + + properties.add(DomainUtil.getPropertyDescriptor(dp)); + } + + columnMap.put(columnName, properties); + } + + // Notably, this method does not commit/save the changes made on the underlying domain. + + return columnMap; + } + + @Override + public void updateReplicateStatsDomain( + User user, + ExpProtocol protocol, + GWTDomain original, + GWTDomain update + ) throws ValidationException + { + var replicateDomain = ensurePlateReplicateStatsDomain(protocol); + var existingReplicateFields = getExistingFields(replicateDomain); + + var originalFields = new HashMap(); + for (var field : original.getFields()) + originalFields.put(field.getPropertyId(), field); + + var domainDirty = false; + var fieldsToRemove = new ArrayList(); + + for (var updateField : update.getFields()) { - // for measures of type : numeric create the stats fields - if (prop.isMeasure()) + var propertyId = updateField.getPropertyId(); + var isNew = !originalFields.containsKey(propertyId); + var isValidType = updateField.isMeasure() && PropertyType.getFromURI(null, updateField.getRangeURI()).getJdbcType().isNumeric(); + + if (isNew) { - PropertyType type = PropertyType.getFromURI(null, prop.getRangeURI()); - if (type.getJdbcType().isNumeric()) + if (isValidType) { - for (String name : PlateReplicateStatsDomainKind.getStatsFieldNames(prop.getName())) + for (var name : PlateReplicateStatsDomainKind.getStatsFieldNames(updateField.getName())) { - // check for additions - if (!existingFields.containsKey(name)) - { - // create the property and copy the format - PropertyStorageSpec spec = new PropertyStorageSpec(name, JdbcType.DOUBLE); + addField(replicateDomain, name); + domainDirty = true; + } + } + } + else + { + var originalField = originalFields.get(propertyId); + var renamed = !originalField.getName().equals(updateField.getName()); + var wasValidType = originalField.isMeasure() && PropertyType.getFromURI(null, originalField.getRangeURI()).getJdbcType().isNumeric(); - // Default formatting is 4 decimal places - DomainProperty domainProperty = replicateDomain.addProperty(spec); - domainProperty.setFormat(prop.getFormat() == null ? "#.####" : prop.getFormat()); + if (isValidType) + { + if (wasValidType) + { + if (renamed) + { + var originalNames = PlateReplicateStatsDomainKind.getStatsFieldNames(originalField.getName()); + var updatedNames = PlateReplicateStatsDomainKind.getStatsFieldNames(updateField.getName()); + for (int i = 0; i < originalNames.size(); i++) + { + var name = originalNames.get(i); + if (existingReplicateFields.containsKey(name)) + { + var updatedName = updatedNames.get(i); + var dp = replicateDomain.getPropertyByName(name); + dp.setName(updatedName); + domainDirty = true; + } + } + } + } + else + { + // something else to numeric measure + for (var name : PlateReplicateStatsDomainKind.getStatsFieldNames(updateField.getName())) + { + addField(replicateDomain, name); domainDirty = true; } - else - existingFields.remove(name); + } + } + else if (wasValidType) + { + // numeric measure to something else + var fieldName = renamed ? originalField.getName() : updateField.getName(); + for (var name : PlateReplicateStatsDomainKind.getStatsFieldNames(fieldName)) + { + var field = existingReplicateFields.get(name); + if (field != null) + fieldsToRemove.add(field); } } } + + originalFields.remove(propertyId); } - // check for removals - if (!existingFields.isEmpty()) + // The only fields that remain in "originalFields" are ones that no longer exist in the updated domain. + // Remove any related replicate fields. + for (var originalField : originalFields.values()) + { + var wasValidType = originalField.isMeasure() && PropertyType.getFromURI(null, originalField.getRangeURI()).getJdbcType().isNumeric(); + if (wasValidType) + { + var fieldName = originalField.getName(); + for (var name : PlateReplicateStatsDomainKind.getStatsFieldNames(fieldName)) + { + var field = existingReplicateFields.get(name); + if (field != null) + fieldsToRemove.add(field); + } + } + } + + if (!fieldsToRemove.isEmpty()) { domainDirty = true; - for (DomainProperty prop : existingFields.values()) + for (DomainProperty prop : fieldsToRemove) prop.delete(); } if (domainDirty) - replicateDomain.save(user); + { + try + { + replicateDomain.save(user); + } + catch (ExperimentException e) + { + throw new ValidationException(e.getMessage()); + } + } } @Override public @Nullable Domain getPlateReplicateStatsDomain(ExpProtocol protocol) { - String uri = getPlateReplicateStatsDomainUri(protocol); - return PropertyService.get().getDomain(protocol.getContainer(), uri); + return getPlateReplicateStatsDomain(protocol.getContainer(), protocol.getName()); } - private String getPlateReplicateStatsDomainUri(ExpProtocol protocol) + private @Nullable Domain getPlateReplicateStatsDomain(Container container, String protocolName) { - DomainKind domainKind = PropertyService.get().getDomainKindByName(PlateReplicateStatsDomainKind.KIND_NAME); - return domainKind.generateDomainURI(AssaySchema.NAME, protocol.getName(), protocol.getContainer(), null); + String uri = getPlateReplicateStatsDomainUri(container, protocolName); + return PropertyService.get().getDomain(container, uri); + } + + private String getPlateReplicateStatsDomainUri(Container container, String protocolName) + { + var domainKind = PropertyService.get().getDomainKindByName(PlateReplicateStatsDomainKind.KIND_NAME); + return domainKind.generateDomainURI(AssaySchema.NAME, protocolName, container, null); } private @NotNull Domain ensurePlateReplicateStatsDomain(ExpProtocol protocol) { - Domain domain = getPlateReplicateStatsDomain(protocol); + return ensurePlateReplicateStatsDomain(protocol.getContainer(), protocol.getName()); + } + + private @NotNull Domain ensurePlateReplicateStatsDomain(Container container, String protocolName) + { + Domain domain = getPlateReplicateStatsDomain(container, protocolName); if (domain == null) - domain = PropertyService.get().createDomain(protocol.getContainer(), getPlateReplicateStatsDomainUri(protocol), PlateReplicateStatsDomainKind.NAME); + domain = PropertyService.get().createDomain(container, getPlateReplicateStatsDomainUri(container, protocolName), PlateReplicateStatsDomainKind.NAME); return domain; } @@ -940,7 +1091,7 @@ public void insertReplicateStats( ExpProtocol protocol, @NotNull ExpRun run, Map>> replicateRows - ) throws ExperimentException + ) throws ValidationException { insertOrUpdateReplicateStats(container, user, protocol, run, true, replicateRows); } @@ -951,7 +1102,7 @@ public void updateReplicateStats( User user, ExpProtocol protocol, Map>> replicateRows - ) throws ExperimentException + ) throws ValidationException { insertOrUpdateReplicateStats(container, user, protocol, null, false, replicateRows); } @@ -963,13 +1114,13 @@ private void insertOrUpdateReplicateStats( @Nullable ExpRun run, boolean forInsert, Map>> replicateRows - ) throws ExperimentException + ) throws ValidationException { if (replicateRows.isEmpty()) return; if (run == null && forInsert) - throw new ExperimentException("Run is required when inserting into the replicate stats table"); + throw new ValidationException("Run is required when inserting into the replicate stats table"); AssayProvider provider = requireProvider(protocol); Domain resultDomain = provider.getResultsDomain(protocol); @@ -1055,7 +1206,7 @@ private void insertOrUpdateReplicateStats( } @Nullable - private static DataState getStateFromRow(Container container, Map row, @Nullable DomainProperty stateProp) throws ExperimentException + private static DataState getStateFromRow(Container container, Map row, @Nullable DomainProperty stateProp) throws ValidationException { if (stateProp != null) { @@ -1069,7 +1220,7 @@ private static DataState getStateFromRow(Container container, Map AssayResultDomainKind.STATE_COLUMN_NAME.equalsIgnoreCase(dp.getName())) + return resultDomain.getProperties().stream().filter(dp -> AssayResultDomainKind.Column.State.name().equalsIgnoreCase(dp.getName())) .findFirst().orElse(null); } @@ -1096,29 +1247,21 @@ public static DomainProperty getAssayStateProp(Domain resultDomain) @Nullable public static DataState validateRowDataStates(Container container, Map row, DomainProperty stateProp) throws ValidationException { - try + DataState state = getStateFromRow(container, row, stateProp); + if (state != null) { - DataState state = getStateFromRow(container, row, stateProp); - if (state != null) - { - if (PlateDataStateManager.StateType.getType(state.getStateType()) == null) - { - throw new ValidationException(String.format("The data state '%s' is not valid for this assay.", state.getLabel())); - } - } - return state; - } - catch (ExperimentException e) - { - throw UnexpectedException.wrap(e); + if (PlateDataStateManager.StateType.getType(state.getStateType()) == null) + throw new ValidationException(String.format("The data state '%s' is not valid for this assay.", state.getLabel())); } + + return state; } - private @NotNull AssayProvider requireProvider(ExpProtocol protocol) throws ExperimentException + private @NotNull AssayProvider requireProvider(ExpProtocol protocol) throws ValidationException { AssayProvider provider = AssayService.get().getProvider(protocol); if (provider == null) - throw new ExperimentException(String.format("Unable to find the provider for protocol : %s", protocol.getName())); + throw new ValidationException(String.format("Unable to find the provider for protocol : %s", protocol.getName())); return provider; } @@ -1129,7 +1272,7 @@ private QueryUpdateService getReplicateStatsUpdateService( User user, AssayProvider provider, ExpProtocol protocol - ) throws ExperimentException + ) throws ValidationException { QueryUpdateService qus = null; AssayProtocolSchema schema = provider.createProtocolSchema(user, container, protocol, null); @@ -1141,7 +1284,7 @@ private QueryUpdateService getReplicateStatsUpdateService( } if (qus == null) - throw new ExperimentException(String.format("There is no replicate stats update service available for assay : %s", protocol.getName())); + throw new ValidationException(String.format("There is no replicate stats update service available for assay : %s", protocol.getName())); return qus; } @@ -1152,7 +1295,7 @@ public void deleteReplicateStats( User user, ExpProtocol protocol, List> keys - ) throws ExperimentException + ) throws ValidationException { if (keys.isEmpty()) return; @@ -1170,6 +1313,151 @@ public void deleteReplicateStats( } } + @Override + public void applyHitSelectionCriteria( + Container container, + User user, + ExpProtocol protocol, + TableInfo table, + List runIds + ) throws ValidationException + { + if (runIds.isEmpty()) + return; + + var provider = requireProvider(protocol); + var filterCriteria = provider.getFilterCriteria(protocol); + if (filterCriteria.isEmpty()) + return; + + var domain = table.getDomain(); + if (domain == null) + { + LOG.error("Automatic hit selection failed. Unable to resolve domain from table ({}).", table); + return; + } + + var url = new ActionURL(); + var replicateDomain = AssayPlateMetadataService.get().getPlateReplicateStatsDomain(protocol); + + for (var criteria : filterCriteria) + { + var domainProperty = domain.getProperty(criteria.propertyId()); + boolean isReplicateProperty = false; + + if (domainProperty == null && replicateDomain != null) + { + domainProperty = replicateDomain.getProperty(criteria.propertyId()); + isReplicateProperty = domainProperty != null; + } + + if (domainProperty == null) + { + LOG.error("Automatic hit selection failed. Unable to resolve domain property from propertyId ({}).", criteria.propertyId()); + return; + } + + FieldKey fieldKey; + if (isReplicateProperty) + fieldKey = FieldKey.fromParts(AssayResultDomainKind.Column.Replicate.name(), domainProperty.getName()); + else + fieldKey = FieldKey.fromParts(domainProperty.getName()); + + var ct = CompareType.getByURLKey(criteria.operation()); + if (ct == null) + { + LOG.error("Automatic hit selection failed. Unable to resolve filter comparison type from operation \"{}\".", criteria.operation()); + return; + } + + url.addFilter(null, fieldKey, ct, criteria.value()); + } + + var filter = new SimpleFilter(); + + // Applying filters via ActionURL allows for automatic type coercion of the filter value + filter.addUrlFilters(url, null); + + // Generate the description for the applied filter criteria prior to incorporating additional clauses + var criteriaDescription = generateFilterCriteriaDescription(filter); + + // The referenced plate well must have a sample value + filter.addCondition(FieldKey.fromParts("Well", "SampleId"), null, CompareType.NONBLANK); + + // Filter out result rows that are excluded + filterOutExcludedRows(container, table, filter); + + // Remove previous hits against the runs that have been modified + PlateManager.get().deleteHitsForRuns(runIds); + + var matchingResults = new TableSelector(table, Collections.singleton(table.getColumn(FieldKey.fromParts("RowId"))), filter, null).getArrayList(Integer.class); + + try + { + if (!matchingResults.isEmpty()) + PlateManager.get().markHits(container, user, protocol.getRowId(), true, matchingResults, null); + } + catch (SQLException e) + { + throw new RuntimeSQLException(e); + } + + var runDomain = provider.getRunDomain(protocol); + if (runDomain != null) + { + var property = runDomain.getPropertyByName(HIT_SELECTION_CRITERIA_COLUMN_NAME); + if (property != null) + { + var pd = property.getPropertyDescriptor(); + for (var run : ExperimentService.get().getExpRuns(runIds)) + { + var value = run.getProperty(pd); + if (!criteriaDescription.equals(value)) + run.setProperty(user, pd, criteriaDescription); + } + } + } + } + + private static void filterOutExcludedRows(Container container, TableInfo table, SimpleFilter filter) + { + PlateDataStateManager stateManager = PlateDataStateManager.get(); + var exclusionStateRowIds = stateManager.getStates(container) + .stream() + .filter(state -> !stateManager.isOperationPermitted(state, PlateDataStateManager.DataOperation.hitSelection)) + .map(DataState::getRowId) + .toList(); + + if (!exclusionStateRowIds.isEmpty()) + filter.addCondition(table.getColumn(AssayResultDomainKind.Column.State.name()), exclusionStateRowIds, CompareType.NOT_IN); + } + + private static String generateFilterCriteriaDescription(SimpleFilter filter) + { + var formatter = new SimpleFilter.ColumnNameFormatter() + { + @Override + public String format(FieldKey fieldKey) + { + var formatted = super.format(fieldKey); + var dotIndex = formatted.lastIndexOf('.'); + if (dotIndex >= 0) + formatted = formatted.substring(dotIndex + 1); + return formatted; + } + }; + + var parts = new ArrayList(); + for (var clause : filter.getClauses()) + { + var sub = new StringBuilder(); + clause.appendFilterText(sub, formatter); + parts.add(sub.toString()); + } + + return StringUtils.join(parts, " and "); + } + private static class PlateMetadataImportHelper extends SimpleAssayDataImportHelper { private final Map> _wellPositionMap; // map of plate position to well table @@ -1216,8 +1504,8 @@ public void bindAdditionalParameters(Map map, ParameterMapStatem Domain resultDomain = _provider.getResultsDomain(_protocol); _stateProp = AssayPlateMetadataServiceImpl.getAssayStateProp(resultDomain); DomainProperty plateSetProperty = runDomain.getPropertyByName(AssayPlateMetadataService.PLATE_SET_COLUMN_NAME); - DomainProperty plateProperty = resultDomain.getPropertyByName(AssayResultDomainKind.PLATE_COLUMN_NAME); - DomainProperty wellLocationProperty = resultDomain.getPropertyByName(AssayResultDomainKind.WELL_LOCATION_COLUMN_NAME); + DomainProperty plateProperty = resultDomain.getPropertyByName(AssayResultDomainKind.Column.Plate.name()); + DomainProperty wellLocationProperty = resultDomain.getPropertyByName(AssayResultDomainKind.Column.WellLocation.name()); // get the plate associated with this row (checking the results domain field first) Object plateIdentifier = PropertyService.get().getDomainPropertyValueFromRow(plateProperty, map); @@ -1273,13 +1561,13 @@ public void bindAdditionalParameters(Map map, ParameterMapStatem // need to adjust the column value to be 0 based to match the template locations pos.setCol(pos.getColumn() - 1); if (positionToWellLsid.containsKey(pos)) - target.put(WELL_LSID_COLUMN_NAME, positionToWellLsid.get(pos)); + target.put(AssayResultDomainKind.Column.WellLsid.name(), positionToWellLsid.get(pos)); // find the associated replicate well group for this position (if any) if (positionToReplicateLsid.containsKey(pos)) { Lsid lsid = positionToReplicateLsid.get(pos); - target.put(REPLICATE_LSID_COLUMN_NAME, lsid); + target.put(AssayResultDomainKind.Column.ReplicateLsid.name(), lsid); _replicateRows.computeIfAbsent(lsid, k -> new ArrayList<>()).add(map); } } @@ -1306,15 +1594,15 @@ public void afterBatchInsert(int rowCount) // compute replicate calculations and insert into the replicate stats table AssayPlateMetadataService.get().insertReplicateStats(_container, _user, _protocol, _run, _replicateRows); + AssayProtocolSchema schema = _provider.createProtocolSchema(_user, _container, _protocol, null); + TableInfo resultsTable = schema.createDataTable(null, false); + // re-select any hits that were present in the previous run if (isExistingRun()) { ExpRun prevRun = ExperimentService.get().getExpRun(_context.getReRunId()); if (prevRun != null) { - AssayProtocolSchema schema = _provider.createProtocolSchema(_user, _container, _protocol, null); - TableInfo resultsTable = schema.createDataTable(null, false); - SQLFragment sql = new SQLFragment("SELECT AR.rowId FROM ").append(resultsTable, "AR") .append(" JOIN ").append(AssayDbSchema.getInstance().getTableInfoHit(), "HT") .append(" ON AR.welllsid = HT.welllsid") @@ -1327,9 +1615,12 @@ public void afterBatchInsert(int rowCount) PlateManager.get().markHits(_container, _user, _protocol.getRowId(), true, rowIds, null); // remove the selections from the previous run - PlateManager.get().deleteHits(FieldKey.fromParts("RunId"), List.of(prevRun)); + PlateManager.get().deleteHitsForRuns(List.of(prevRun.getRowId())); } } + + AssayPlateMetadataService.get().applyHitSelectionCriteria(_container, _user, _protocol, resultsTable, List.of(_run.getRowId())); + tx.commit(); } catch (Throwable e) diff --git a/assay/src/org/labkey/assay/plate/AssayPlateTriggerFactory.java b/assay/src/org/labkey/assay/plate/AssayPlateTriggerFactory.java index 50cf0d6d72e..b4893796ff9 100644 --- a/assay/src/org/labkey/assay/plate/AssayPlateTriggerFactory.java +++ b/assay/src/org/labkey/assay/plate/AssayPlateTriggerFactory.java @@ -4,7 +4,6 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.AssayProvider; import org.labkey.api.assay.AssayResultDomainKind; -import org.labkey.api.assay.AssayService; import org.labkey.api.assay.plate.AssayPlateMetadataService; import org.labkey.api.assay.plate.PlateDataStateManager; import org.labkey.api.data.CompareType; @@ -15,9 +14,9 @@ import org.labkey.api.data.TableSelector; import org.labkey.api.data.triggers.Trigger; import org.labkey.api.data.triggers.TriggerFactory; -import org.labkey.api.exp.ExperimentException; import org.labkey.api.exp.Lsid; import org.labkey.api.exp.api.ExpProtocol; +import org.labkey.api.exp.api.ExperimentService; import org.labkey.api.exp.property.DomainProperty; import org.labkey.api.qc.DataState; import org.labkey.api.query.BatchValidationException; @@ -37,19 +36,15 @@ public class AssayPlateTriggerFactory implements TriggerFactory { + private final AssayProvider _provider; private final ExpProtocol _protocol; - private DomainProperty _qcStateProp; + private final DomainProperty _qcStateProp; - public AssayPlateTriggerFactory(ExpProtocol protocol) + public AssayPlateTriggerFactory(@NotNull AssayProvider provider, @NotNull ExpProtocol protocol) { + _provider = provider; _protocol = protocol; - - if (_protocol != null) - { - AssayProvider provider = AssayService.get().getProvider(_protocol); - if (provider != null) - _qcStateProp = AssayPlateMetadataServiceImpl.getAssayStateProp(provider.getResultsDomain(_protocol)); - } + _qcStateProp = AssayPlateMetadataServiceImpl.getAssayStateProp(provider.getResultsDomain(_protocol)); } @Override @@ -57,7 +52,8 @@ public AssayPlateTriggerFactory(ExpProtocol protocol) { return List.of( new ReplicateStatsTrigger(), - new DataStateTrigger() + new DataStateTrigger(), + new AutomaticHitSelectionTrigger() ); } @@ -74,7 +70,7 @@ private void checkForChanges(@Nullable Map oldRow, boolean isUpd if (oldRow != null) { // check if the change is to a replicate well row - Object replicateLsid = oldRow.get(AssayResultDomainKind.REPLICATE_LSID_COLUMN_NAME); + Object replicateLsid = oldRow.get(AssayResultDomainKind.Column.ReplicateLsid.name()); if (replicateLsid != null) _replicateLsid.put(String.valueOf(replicateLsid), isUpdate); } @@ -104,7 +100,7 @@ public void complete(TableInfo table, Container c, User user, TableInfo.TriggerT if (_replicateLsid.isEmpty() || errors.hasErrors()) return; - var filter = new SimpleFilter(FieldKey.fromParts(AssayResultDomainKind.REPLICATE_LSID_COLUMN_NAME), _replicateLsid.keySet(), CompareType.IN); + var filter = new SimpleFilter(FieldKey.fromParts(AssayResultDomainKind.Column.ReplicateLsid.name()), _replicateLsid.keySet(), CompareType.IN); try (TableResultSet rs = new TableSelector(table, filter, null).getResultSet()) { @@ -112,7 +108,7 @@ public void complete(TableInfo table, Container c, User user, TableInfo.TriggerT while (rs.next()) { - var lsid = rs.getString(AssayResultDomainKind.REPLICATE_LSID_COLUMN_NAME); + var lsid = rs.getString(AssayResultDomainKind.Column.ReplicateLsid.name()); replicates.computeIfAbsent(Lsid.parse(String.valueOf(lsid)), m -> new ArrayList<>()).add(rs.getRowMap()); _replicateLsid.remove(lsid); } @@ -130,7 +126,11 @@ public void complete(TableInfo table, Container c, User user, TableInfo.TriggerT AssayPlateMetadataService.get().updateReplicateStats(c, user, _protocol, replicates); } - catch (ExperimentException | SQLException e) + catch (ValidationException ve) + { + errors.addRowError(ve); + } + catch (SQLException e) { throw UnexpectedException.wrap(e); } @@ -146,8 +146,19 @@ private class DataStateTrigger implements Trigger Set _excludedRows = new HashSet<>(); @Override - public void beforeUpdate(TableInfo table, Container c, User user, @Nullable Map newRow, @Nullable Map oldRow, ValidationException errors, Map extraContext) throws ValidationException + public void beforeUpdate( + TableInfo table, + Container c, + User user, + @Nullable Map newRow, + @Nullable Map oldRow, + ValidationException errors, + Map extraContext + ) throws ValidationException { + if (errors.hasErrors()) + return; + if (newRow != null && _qcStateProp != null) { DataState state = AssayPlateMetadataServiceImpl.validateRowDataStates(c, newRow, _qcStateProp); @@ -168,4 +179,78 @@ public void complete(TableInfo table, Container c, User user, TableInfo.TriggerT PlateManager.get().deleteHits(_protocol.getRowId(), _excludedRows); } } + + private class AutomaticHitSelectionTrigger implements Trigger + { + private Set dataIds = null; + private boolean enabled = false; + + @Override + public void init( + TableInfo table, + Container c, + User user, + TableInfo.TriggerType event, + BatchValidationException errors, + Map extraContext + ) + { + if (errors.hasErrors()) + return; + + if (_provider.hasFilterCriteria(_protocol)) + { + enabled = true; + dataIds = new HashSet<>(); + } + } + + @Override + public void afterUpdate( + TableInfo table, + Container c, + User user, + @Nullable Map newRow, + @Nullable Map oldRow, + ValidationException errors, + Map extraContext + ) + { + if (!enabled || errors.hasErrors() || oldRow == null) + return; + + dataIds.add(((Number) oldRow.get("DataId")).intValue()); + } + + @Override + public void complete( + TableInfo table, + Container c, + User user, + TableInfo.TriggerType event, + BatchValidationException errors, + Map extraContext + ) + { + if (!enabled || errors.hasErrors()) + return; + + List runIds = new ArrayList<>(); + for (var expDataId : dataIds) + { + var data = ExperimentService.get().getExpData(expDataId); + if (data != null) + runIds.add(data.getRunId()); + } + + try + { + AssayPlateMetadataService.get().applyHitSelectionCriteria(c, user, _protocol, table, runIds); + } + catch (ValidationException e) + { + errors.addRowError(e); + } + } + } } diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index e674f6bfce3..9ef4d994e73 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -374,7 +374,7 @@ private void deriveCustomFieldsFromWellData( @Nullable PlateSet plateSet ) throws Exception { - requireActiveTransaction(); + assert requireActiveTransaction(); Set customFields = new LinkedHashSet<>(getDefaultFieldsForPlateSet(plate, plateSet)); @@ -1323,7 +1323,7 @@ public void afterPlateDelete(Container container, Plate plate) // Called by the Plate Query Update Service prior to deleting a plate public void beforePlateDelete(Container container, Integer plateId) { - requireActiveTransaction(); + assert requireActiveTransaction(); Plate plate = PlateCache.getPlate(container, plateId); List lsids = new ArrayList<>(); @@ -1362,7 +1362,7 @@ public void beforePlateSetDelete(Container container, User user, Integer rowId) private void beforePlateSetsDelete(Collection plateSetIds, Container container) { - requireActiveTransaction(); + assert requireActiveTransaction(); if (plateSetIds.isEmpty()) return; @@ -1695,7 +1695,7 @@ private void copyProperties(@NotNull Plate source, @NotNull Plate copy) private void copyWellData(User user, @NotNull Plate source, @NotNull Plate copy, boolean copySample) throws Exception { - requireActiveTransaction(); + assert requireActiveTransaction(); var container = source.getContainer(); var wellTable = getWellTable(container, user); @@ -2740,7 +2740,7 @@ public PlateSet replatePlateSet( private void savePlateSetHeritage(Integer plateSetId, PlateSetType plateSetType, @Nullable PlateSetImpl parentPlateSet) { - requireActiveTransaction(); + assert requireActiveTransaction(); // Configure rootPlateSetId Integer rootPlateSetId = null; @@ -3110,11 +3110,22 @@ public void deleteHits(FieldKey fieldKey, Collection object public void deleteHits(int protocolId, Collection resultIds) { + if (resultIds == null || resultIds.isEmpty()) + return; + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("ProtocolId"), protocolId); filter.addCondition(FieldKey.fromParts("ResultId"), resultIds, CompareType.IN); deleteHits(filter); } + public void deleteHitsForRuns(Collection runIds) + { + if (runIds == null || runIds.isEmpty()) + return; + + deleteHits(new SimpleFilter(FieldKey.fromParts("RunId"), runIds, CompareType.IN)); + } + private void deleteReplicateStats(ExpProtocol protocol, User user, SimpleFilter filter) { AssayProvider provider = AssayService.get().getProvider(protocol); @@ -3239,10 +3250,9 @@ public void validatePrimaryPlateSetUniqueSamples(Set wellRowIds, BatchV } } - private void requireActiveTransaction() + private boolean requireActiveTransaction() { - if (!AssayDbSchema.getInstance().getSchema().getScope().isTransactionActive()) - throw new IllegalStateException("This method must be called from within a transaction"); + return AssayDbSchema.getInstance().getSchema().getScope().isTransactionActive(); } Pair>> getWellSampleData( @@ -3656,7 +3666,7 @@ public void computeWellGroups( Map> wellGroupChanges ) throws ValidationException { - requireActiveTransaction(); + assert requireActiveTransaction(); if (wellGroupChanges.isEmpty()) return; diff --git a/assay/src/org/labkey/assay/plate/PlateMetricsProvider.java b/assay/src/org/labkey/assay/plate/PlateMetricsProvider.java index ff2427499da..139d332bc39 100644 --- a/assay/src/org/labkey/assay/plate/PlateMetricsProvider.java +++ b/assay/src/org/labkey/assay/plate/PlateMetricsProvider.java @@ -28,10 +28,10 @@ public class PlateMetricsProvider implements UsageMetricsProvider { private SQLFragment plateSetPlatesSQL(TableInfo plateSetTable, TableInfo plateTable) { - return new SQLFragment("SELECT ps.rowId, COUNT(p.rowid) AS plateCount FROM ") + return new SQLFragment("SELECT ps.rowId, COUNT(p.rowId) AS plateCount FROM ") .append(plateSetTable, "ps") .append(" LEFT OUTER JOIN ").append(plateTable, "p") - .append(" ON ps.rowid = p.plateset") + .append(" ON ps.rowId = p.plateSet") .append(" WHERE ps.template = ? AND ps.archived = ?") .append(" GROUP BY ps.rowId") .add(false) @@ -84,7 +84,7 @@ private Long plateTypeCount(DbSchema schema, TableInfo plateTable, TableInfo pla SQLFragment sql = new SQLFragment("SELECT COUNT(*) FROM ") .append(plateTable, "p") .append(" JOIN ").append(plateTypeTable, "pt") - .append(" ON p.platetype = pt.rowid") + .append(" ON p.plateType = pt.rowId") .append(" WHERE pt.columns = ? AND pt.rows = ?") .add(cols) .add(rows); @@ -103,11 +103,11 @@ private List getBiologicsFolders() private List getPlateEnabledAssayProtocols() { AssayProvider provider = AssayService.get().getProvider(TsvAssayProvider.NAME); - if (provider == null) return Collections.emptyList(); - var containers = getBiologicsFolders(); - List allPlateProtocols = new ArrayList<>(); + if (provider == null) + return Collections.emptyList(); - for (Container c : containers) + List allPlateProtocols = new ArrayList<>(); + for (Container c : getBiologicsFolders()) { List plateProtocols = AssayService.get().getAssayProtocols(c).stream().filter(provider::isPlateMetadataEnabled).toList(); allPlateProtocols.addAll(plateProtocols); @@ -182,14 +182,14 @@ public Map getUsageMetrics() TableInfo plateTable = schema.getTableInfoPlate(); Long plateSetCount = plateSetCount(schema.getSchema(), plateSetTable, false); Long archivedPlateSetCount = plateSetCount(schema.getSchema(), plateSetTable, true); - Long primaryPlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type =?").add(PlateSetType.primary)).getObject(Long.class); - Long assayPlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type =?").add(PlateSetType.assay)).getObject(Long.class); - Long standAlonePlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type =?").add(PlateSetType.assay).append(" AND rootplatesetid IS NULL")).getObject(Long.class); + Long primaryPlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type = ?").add(PlateSetType.primary)).getObject(Long.class); + Long assayPlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type = ?").add(PlateSetType.assay)).getObject(Long.class); + Long standAlonePlateSetCount = new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(plateSetTable, "ps").append(" WHERE type = ?").add(PlateSetType.assay).append(" AND rootPlateSetId IS NULL")).getObject(Long.class); Long plateSetNoPlatesCount = plateSetPlatesCount(schema.getSchema(), plateSetTable, plateTable, 0); Long plateSetOnePlateCount = plateSetPlatesCount(schema.getSchema(), plateSetTable, plateTable, 1); SQLFragment maxPlatesSql = new SQLFragment("SELECT MAX(plateCount) FROM (").append(plateSetPlatesSQL(plateSetTable, plateTable)).append(") x"); Long maxPlatesCount = new SqlSelector(schema.getSchema(), maxPlatesSql).getObject(Long.class); - // too many items to use Map.of() + Map plateSets = new HashMap<>(); plateSets.put("archivedPlateSetCount", archivedPlateSetCount); plateSets.put("plateSetCount", plateSetCount); @@ -211,30 +211,33 @@ public Map getUsageMetrics() TableInfo plateTypeTable = schema.getTableInfoPlateType(); TableInfo wellTable = schema.getTableInfoWell(); plateMetrics.put("plates", Map.of( - "platesCount", platesCount, - "archivedPlatesCount", archivedPlatesCount, - "plateTemplateCount", plateTemplateCount, - "archivedTemplatesCount", archivedPlateTemplates, - "distinctPlatedSamples", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM (SELECT DISTINCT sampleId FROM ").append(wellTable, "w").append(" WHERE sampleId IS NOT NULL) as ds")).getObject(Long.class), - "12WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 4, 3), - "24WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 6, 4), - "48WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 8, 6), - "96WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 12, 8), - "384WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 24, 16) + "platesCount", platesCount, + "archivedPlatesCount", archivedPlatesCount, + "plateTemplateCount", plateTemplateCount, + "archivedTemplatesCount", archivedPlateTemplates, + "distinctPlatedSamples", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM (SELECT DISTINCT sampleId FROM ").append(wellTable, "w").append(" WHERE sampleId IS NOT NULL) as ds")).getObject(Long.class), + "12WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 4, 3), + "24WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 6, 4), + "48WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 8, 6), + "96WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 12, 8), + "384WellCount", plateTypeCount(schema.getSchema(), plateTable, plateTypeTable, 24, 16) )); TableInfo hitTable = schema.getTableInfoHit(); + TableInfo filterCriteriaTable = schema.getTableInfoFilterCriteria(); List plateEnabledProtocols = getPlateEnabledAssayProtocols(); plateMetrics.put("assays", Map.of( - "hitCount", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(hitTable, "h")).getObject(Long.class), - "plateSetsWithHits", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(DISTINCT platesetpath) FROM ").append(hitTable, "h")).getObject(Long.class), - "assaysWithPlateMetadataEnabled", plateEnabledProtocols.size(), - "assayRunsCount", getPlateBasedAssayRunsCount(plateEnabledProtocols), - "assayResultsCount", getPlateBasedAssayResultsCount(plateEnabledProtocols) + "hitCount", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(*) FROM ").append(hitTable, "h")).getObject(Long.class), + "plateSetsWithHits", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(DISTINCT plateSetPath) FROM ").append(hitTable, "h")).getObject(Long.class), + "assaysWithPlateMetadataEnabled", plateEnabledProtocols.size(), + "assayRunsCount", getPlateBasedAssayRunsCount(plateEnabledProtocols), + "assayResultsCount", getPlateBasedAssayResultsCount(plateEnabledProtocols), + "domainsWithFilterCriteriaConfigured", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(DISTINCT domainId) FROM ").append(filterCriteriaTable, "fc")).getObject(Long.class), + "columnsWithFilterCriteria", new SqlSelector(schema.getSchema(), new SQLFragment("SELECT COUNT(DISTINCT propertyId) FROM ").append(filterCriteriaTable, "fc")).getObject(Long.class) )); plateMetrics.put("metadata", Map.of( - "fieldsCount", getMetadataFieldsCount() + "fieldsCount", getMetadataFieldsCount() )); return Map.of("plates", plateMetrics); diff --git a/assay/src/org/labkey/assay/query/AssayDbSchema.java b/assay/src/org/labkey/assay/query/AssayDbSchema.java index 065fe909475..0cb9f1d2ba9 100644 --- a/assay/src/org/labkey/assay/query/AssayDbSchema.java +++ b/assay/src/org/labkey/assay/query/AssayDbSchema.java @@ -78,4 +78,9 @@ public TableInfo getTableInfoHit() { return getSchema().getTable("Hit"); } + + public TableInfo getTableInfoFilterCriteria() + { + return getSchema().getTable("FilterCriteria"); + } } diff --git a/core/package-lock.json b/core/package-lock.json index ab6f8893953..ea8d7139df8 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": "6.6.0", + "@labkey/components": "6.8.0", "@labkey/themes": "1.4.0" }, "devDependencies": { @@ -3017,9 +3017,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.36.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.36.0.tgz", - "integrity": "sha512-cWQd1Umwkg7H/KLWpQ0I3p7GfLHw8kwFVAAtJZDeaykd21lyIypyOgQq+gLvmQJTAi9vRP4eaJ85L+b4o4x9Gw==" + "version": "1.37.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.37.0.tgz", + "integrity": "sha512-PIuzYGEm0O6ydWoXWEqCV+hHGqzDsVZ5Q3JD6i/d/bvp6On0jML9lnmz45hw4ZAXiwLSpd09whaTeSPVxnDxig==" }, "node_modules/@labkey/build": { "version": "8.3.0", @@ -3058,12 +3058,12 @@ } }, "node_modules/@labkey/components": { - "version": "6.6.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.6.0.tgz", - "integrity": "sha512-OVmQjchyUr8VlaKaSDTg+QpGjImEm+dWhnfctvK8uRLb6GynNAkgL7nPv/Bt+awRwtv5L66qXUeSSOcIWqV8sg==", + "version": "6.8.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.8.0.tgz", + "integrity": "sha512-wHErvhP7+d0K6rrXUREgpzj5HjMJTfgQO3iJrg/wW9B9408mjMgtW76MGfjdn1haqepnbEIvUlA7u1Y5qS2X5A==", "dependencies": { "@hello-pangea/dnd": "17.0.0", - "@labkey/api": "1.36.0", + "@labkey/api": "1.37.0", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.0.1", diff --git a/core/package.json b/core/package.json index 914f3745ee2..c9ae5452bf7 100644 --- a/core/package.json +++ b/core/package.json @@ -54,7 +54,7 @@ } }, "dependencies": { - "@labkey/components": "6.6.0", + "@labkey/components": "6.8.0", "@labkey/themes": "1.4.0" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 9efb44a315e..330c2308853 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", @@ -2823,9 +2823,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.36.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.36.0.tgz", - "integrity": "sha512-cWQd1Umwkg7H/KLWpQ0I3p7GfLHw8kwFVAAtJZDeaykd21lyIypyOgQq+gLvmQJTAi9vRP4eaJ85L+b4o4x9Gw==" + "version": "1.37.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.37.0.tgz", + "integrity": "sha512-PIuzYGEm0O6ydWoXWEqCV+hHGqzDsVZ5Q3JD6i/d/bvp6On0jML9lnmz45hw4ZAXiwLSpd09whaTeSPVxnDxig==" }, "node_modules/@labkey/build": { "version": "8.3.0", @@ -2864,12 +2864,12 @@ } }, "node_modules/@labkey/components": { - "version": "6.6.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.6.0.tgz", - "integrity": "sha512-OVmQjchyUr8VlaKaSDTg+QpGjImEm+dWhnfctvK8uRLb6GynNAkgL7nPv/Bt+awRwtv5L66qXUeSSOcIWqV8sg==", + "version": "6.8.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.8.0.tgz", + "integrity": "sha512-wHErvhP7+d0K6rrXUREgpzj5HjMJTfgQO3iJrg/wW9B9408mjMgtW76MGfjdn1haqepnbEIvUlA7u1Y5qS2X5A==", "dependencies": { "@hello-pangea/dnd": "17.0.0", - "@labkey/api": "1.36.0", + "@labkey/api": "1.37.0", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.0.1", diff --git a/experiment/package.json b/experiment/package.json index 7332f69d2d1..601fc85cb3b 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": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", diff --git a/experiment/src/org/labkey/experiment/api/data/LineageClause.java b/experiment/src/org/labkey/experiment/api/data/LineageClause.java index 737a09bf4f0..19721739484 100644 --- a/experiment/src/org/labkey/experiment/api/data/LineageClause.java +++ b/experiment/src/org/labkey/experiment/api/data/LineageClause.java @@ -116,7 +116,7 @@ protected String filterTextType() } @Override - protected void appendFilterText(StringBuilder sb, SimpleFilter.ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, SimpleFilter.ColumnNameFormatter formatter) { ExpRunItem start = getStart(); if (start == null) diff --git a/pipeline/package-lock.json b/pipeline/package-lock.json index 98ce7226cba..762172a0f62 100644 --- a/pipeline/package-lock.json +++ b/pipeline/package-lock.json @@ -8,7 +8,7 @@ "name": "pipeline", "version": "0.0.0", "dependencies": { - "@labkey/components": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", @@ -2387,9 +2387,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.36.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.36.0.tgz", - "integrity": "sha512-cWQd1Umwkg7H/KLWpQ0I3p7GfLHw8kwFVAAtJZDeaykd21lyIypyOgQq+gLvmQJTAi9vRP4eaJ85L+b4o4x9Gw==" + "version": "1.37.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.37.0.tgz", + "integrity": "sha512-PIuzYGEm0O6ydWoXWEqCV+hHGqzDsVZ5Q3JD6i/d/bvp6On0jML9lnmz45hw4ZAXiwLSpd09whaTeSPVxnDxig==" }, "node_modules/@labkey/build": { "version": "8.3.0", @@ -2428,12 +2428,12 @@ } }, "node_modules/@labkey/components": { - "version": "6.6.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.6.0.tgz", - "integrity": "sha512-OVmQjchyUr8VlaKaSDTg+QpGjImEm+dWhnfctvK8uRLb6GynNAkgL7nPv/Bt+awRwtv5L66qXUeSSOcIWqV8sg==", + "version": "6.8.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-6.8.0.tgz", + "integrity": "sha512-wHErvhP7+d0K6rrXUREgpzj5HjMJTfgQO3iJrg/wW9B9408mjMgtW76MGfjdn1haqepnbEIvUlA7u1Y5qS2X5A==", "dependencies": { "@hello-pangea/dnd": "17.0.0", - "@labkey/api": "1.36.0", + "@labkey/api": "1.37.0", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.0.1", diff --git a/pipeline/package.json b/pipeline/package.json index 5be82d29c38..54d75781b26 100644 --- a/pipeline/package.json +++ b/pipeline/package.json @@ -14,7 +14,7 @@ "build-prod": "npm run clean && cross-env NODE_ENV=production PROD_SOURCE_MAP=source-map webpack --config node_modules/@labkey/build/webpack/prod.config.js --color --progress --profile" }, "dependencies": { - "@labkey/components": "6.6.0" + "@labkey/components": "6.8.0" }, "devDependencies": { "@labkey/build": "8.3.0", diff --git a/query/src/org/labkey/query/QueryServiceImpl.java b/query/src/org/labkey/query/QueryServiceImpl.java index de86a59fd3e..e0366cdb327 100644 --- a/query/src/org/labkey/query/QueryServiceImpl.java +++ b/query/src/org/labkey/query/QueryServiceImpl.java @@ -489,7 +489,7 @@ private static class WhereClause extends QueryCompareClause } @Override - protected void appendFilterText(StringBuilder sb, SimpleFilter.ColumnNameFormatter formatter) + public void appendFilterText(StringBuilder sb, SimpleFilter.ColumnNameFormatter formatter) { sb.append(getParamVals()[0]); }