diff --git a/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java b/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java index 51418b27794..afd647ac38a 100644 --- a/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java +++ b/api/src/org/labkey/api/assay/plate/AssayPlateMetadataService.java @@ -25,6 +25,7 @@ public interface AssayPlateMetadataService { String PLATE_TEMPLATE_COLUMN_NAME = "PlateTemplate"; Map<AssayDataType, AssayPlateMetadataService> _handlers = new HashMap<>(); + String EXPERIMENTAL_APP_PLATE_SUPPORT = "experimental-app-plate-support"; static void registerService(AssayDataType dataType, AssayPlateMetadataService handler) { @@ -38,7 +39,7 @@ static void registerService(AssayDataType dataType, AssayPlateMetadataService ha static boolean isExperimentalAppPlateEnabled() { - return ExperimentalFeatureService.get().isFeatureEnabled("experimental-app-plate-support"); + return ExperimentalFeatureService.get().isFeatureEnabled(EXPERIMENTAL_APP_PLATE_SUPPORT); } @Nullable diff --git a/assay/api-src/org/labkey/api/assay/plate/Plate.java b/assay/api-src/org/labkey/api/assay/plate/Plate.java index cf0cb83302d..6225d1379ae 100644 --- a/assay/api-src/org/labkey/api/assay/plate/Plate.java +++ b/assay/api-src/org/labkey/api/assay/plate/Plate.java @@ -16,6 +16,7 @@ package org.labkey.api.assay.plate; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.exp.Identifiable; import org.labkey.api.study.PropertySet; @@ -43,6 +44,8 @@ public interface Plate extends PropertySet, Identifiable Well getWell(int rowId); + List<Well> getWells(); + WellGroup getWellGroup(WellGroup.Type type, String wellGroupName); @Nullable WellGroup getWellGroup(int rowId); @@ -71,4 +74,17 @@ public interface Plate extends PropertySet, Identifiable @Override @Nullable ActionURL detailsURL(); + + /** + * The list of metadata fields that are configured for this plate + */ + @NotNull List<PlateCustomField> getCustomFields(); + + Plate copy(); + + /** + * Returns the domain ID for the plate metadata domain. + */ + @Nullable + Integer getMetadataDomainId(); } diff --git a/assay/src/org/labkey/assay/plate/model/PlateCustomField.java b/assay/api-src/org/labkey/api/assay/plate/PlateCustomField.java similarity index 97% rename from assay/src/org/labkey/assay/plate/model/PlateCustomField.java rename to assay/api-src/org/labkey/api/assay/plate/PlateCustomField.java index 2bf207e2b8f..b5aa8b3a022 100644 --- a/assay/src/org/labkey/assay/plate/model/PlateCustomField.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateCustomField.java @@ -1,4 +1,4 @@ -package org.labkey.assay.plate.model; +package org.labkey.api.assay.plate; import com.fasterxml.jackson.annotation.JsonInclude; import org.labkey.api.exp.property.DomainProperty; diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateService.java b/assay/api-src/org/labkey/api/assay/plate/PlateService.java index fbc1967347a..154f227b2b3 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateService.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateService.java @@ -25,6 +25,8 @@ import org.labkey.api.data.statistics.StatsService; import org.labkey.api.exp.Lsid; import org.labkey.api.exp.api.ExpRun; +import org.labkey.api.exp.property.Domain; +import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; import org.labkey.api.services.ServiceRegistry; import org.labkey.api.view.ActionURL; @@ -236,6 +238,11 @@ static PlateService get() */ TableInfo getPlateTableInfo(); + /** + * Create the plate metadata domain for this container. + */ + @NotNull Domain ensurePlateMetadataDomain(Container container, User user) throws ValidationException; + /** * A PlateDetailsResolver implementation provides a URL where a detailed, plate-type specific * UI can be found. diff --git a/assay/api-src/org/labkey/api/assay/plate/PositionImpl.java b/assay/api-src/org/labkey/api/assay/plate/PositionImpl.java index 9734ec43b53..fb2943ea206 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PositionImpl.java +++ b/assay/api-src/org/labkey/api/assay/plate/PositionImpl.java @@ -78,6 +78,12 @@ public int getColumn() return _column; } + // needed for DB serialization + public int getCol() + { + return getColumn(); + } + @Override public int getRow() { @@ -143,11 +149,6 @@ public void setCol(int column) setColumn(column); } - public int getCol() - { - return getColumn(); - } - public Integer getPlateId() { return _plateId; diff --git a/assay/src/org/labkey/assay/plate/model/WellCustomField.java b/assay/api-src/org/labkey/api/assay/plate/WellCustomField.java similarity index 92% rename from assay/src/org/labkey/assay/plate/model/WellCustomField.java rename to assay/api-src/org/labkey/api/assay/plate/WellCustomField.java index 224d5e5f322..f13ea45aa42 100644 --- a/assay/src/org/labkey/assay/plate/model/WellCustomField.java +++ b/assay/api-src/org/labkey/api/assay/plate/WellCustomField.java @@ -1,4 +1,4 @@ -package org.labkey.assay.plate.model; +package org.labkey.api.assay.plate; import org.labkey.api.exp.property.DomainProperty; diff --git a/assay/src/org/labkey/assay/AssayModule.java b/assay/src/org/labkey/assay/AssayModule.java index 93c85497696..02cdfbbea8b 100644 --- a/assay/src/org/labkey/assay/AssayModule.java +++ b/assay/src/org/labkey/assay/AssayModule.java @@ -54,6 +54,7 @@ import org.labkey.api.security.User; import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.security.roles.RoleManager; +import org.labkey.api.settings.AdminConsole; import org.labkey.api.util.ContextListener; import org.labkey.api.util.JspTestCase; import org.labkey.api.util.PageFlowUtil; @@ -89,6 +90,7 @@ import static org.labkey.api.assay.DefaultDataTransformer.LEGACY_SESSION_COOKIE_NAME_REPLACEMENT; import static org.labkey.api.assay.DefaultDataTransformer.LEGACY_SESSION_ID_REPLACEMENT; +import static org.labkey.api.assay.plate.AssayPlateMetadataService.EXPERIMENTAL_APP_PLATE_SUPPORT; public class AssayModule extends SpringModule { @@ -246,6 +248,9 @@ public void moduleStartupComplete(ServletContext servletContext) }); ExperimentService.get().addExperimentListener(new AssayExperimentListener()); + + AdminConsole.addExperimentalFeatureFlag(new AdminConsole.ExperimentalFeatureFlag(EXPERIMENTAL_APP_PLATE_SUPPORT, + "Plate samples in Biologics", "Plate samples in Biologics for import and analysis.", false, true)); } @Override diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index 6ba0652c80b..eb6b935dd36 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -30,10 +30,12 @@ import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateCustomField; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; +import org.labkey.api.exp.property.Domain; import org.labkey.api.gwt.client.model.GWTPropertyDescriptor; import org.labkey.api.gwt.server.BaseRemoteService; import org.labkey.api.security.RequiresAnyOf; @@ -57,9 +59,7 @@ import org.labkey.assay.plate.PlateDataServiceImpl; import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.PlateUrls; -import org.labkey.assay.plate.model.PlateCustomField; import org.labkey.assay.plate.model.PlateType; -import org.labkey.assay.plate.model.WellCustomField; import org.labkey.assay.view.AssayGWTView; import org.springframework.validation.BindException; import org.springframework.validation.Errors; @@ -734,12 +734,12 @@ public Object execute(CustomFieldsForm form, BindException errors) throws Except } @RequiresPermission(ReadPermission.class) - public class GetFieldsAction extends MutatingApiAction<CustomFieldsForm> + public class GetFieldsAction extends ReadOnlyApiAction<CustomFieldsForm> { @Override public Object execute(CustomFieldsForm form, BindException errors) throws Exception { - return success(PlateManager.get().getFields(getContainer(), getUser(), form.getPlateId())); + return success(PlateManager.get().getFields(getContainer(), form.getPlateId())); } } @@ -753,39 +753,50 @@ public Object execute(CustomFieldsForm form, BindException errors) throws Except } } - public static class SetFieldsForm extends CustomFieldsForm + /** + * Returns the Domain ID for the plate metadata configured for this container. If the domain has + * not been created then null will be returned. + */ + @RequiresPermission(ReadPermission.class) + public class GetPlateMetadataDomainAction extends ReadOnlyApiAction<Object> { - private Integer _wellId; - private List<WellCustomField> _wellFields; - - public Integer getWellId() + @Override + public Object execute(Object form, BindException errors) throws Exception { - return _wellId; + Domain domain = PlateManager.get().getPlateMetadataDomain(getContainer(), getUser()); + return success(domain != null ? domain.getTypeId() : null); } + } - public void setWellId(Integer wellId) - { - _wellId = wellId; - } + public static class GetPlateForm + { + private Integer _rowId; - public List<WellCustomField> getWellFields() + public Integer getRowId() { - return _wellFields; + return _rowId; } - public void setWellFields(List<WellCustomField> wellFields) + public void setRowId(Integer rowId) { - _wellFields = wellFields; + _rowId = rowId; } } - @RequiresPermission(UpdatePermission.class) - public class SetFieldsAction extends MutatingApiAction<SetFieldsForm> + @RequiresPermission(ReadPermission.class) + public static class GetPlateAction extends ReadOnlyApiAction<GetPlateForm> { @Override - public Object execute(SetFieldsForm form, BindException errors) throws Exception + public void validateForm(GetPlateForm form, Errors errors) + { + if (form.getRowId() == null) + errors.reject(ERROR_GENERIC, "Plate \"rowId\" is required."); + } + + @Override + public Object execute(GetPlateForm form, BindException errors) throws Exception { - return success(PlateManager.get().setFields(getContainer(), getUser(), form.getPlateId(), form.getWellId(), form.getWellFields())); + return PlateManager.get().getPlate(getContainer(), form.getRowId()); } } } diff --git a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java index db4caaddd44..957df93cc75 100644 --- a/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/AssayPlateMetadataServiceImpl.java @@ -44,7 +44,7 @@ import org.labkey.api.security.User; import org.labkey.assay.TSVProtocolSchema; import org.labkey.assay.plate.model.Well; -import org.labkey.assay.plate.model.WellCustomField; +import org.labkey.api.assay.plate.WellCustomField; import org.labkey.assay.query.AssayDbSchema; import java.io.File; @@ -283,7 +283,7 @@ public List<Map<String, Object>> mergePlateMetadata( throw new ExperimentException("Unable to resolve the plate for the run"); // if there are metadata fields configured for this plate - if (!PlateManager.get().getFields(container, user, plate.getRowId()).isEmpty()) + if (!PlateManager.get().getFields(container, plate.getRowId()).isEmpty()) { // create the map of well locations to the well Map<Position, Well> positionToWell = new HashMap<>(); diff --git a/assay/src/org/labkey/assay/plate/PlateCache.java b/assay/src/org/labkey/assay/plate/PlateCache.java index d96b69464b4..d827a61bae4 100644 --- a/assay/src/org/labkey/assay/plate/PlateCache.java +++ b/assay/src/org/labkey/assay/plate/PlateCache.java @@ -4,121 +4,217 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; import org.labkey.api.assay.plate.Plate; import org.labkey.api.cache.Cache; +import org.labkey.api.cache.CacheLoader; import org.labkey.api.cache.CacheManager; import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.TableSelector; import org.labkey.api.exp.Lsid; +import org.labkey.api.query.FieldKey; import org.labkey.assay.query.AssayDbSchema; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class PlateCache { - private static final Cache<Container, PlateCollections> PLATE_COLLECTIONS_CACHE = CacheManager.getBlockingCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", (c, argument) -> new PlateCollections(c)); + private static final PlateLoader _loader = new PlateLoader(); + private static final Cache<String, Plate> PLATE_CACHE = CacheManager.getBlockingStringKeyCache(CacheManager.UNLIMITED, CacheManager.DAY, "Plate Cache", _loader); private static final Logger LOG = LogManager.getLogger(PlateCache.class); - private static class PlateCollections + private static class PlateLoader implements CacheLoader<String, Plate> { - private final Map<Integer, Plate> _rowIdMap; - private final Map<String, Plate> _nameMap; - private final Map<Lsid, Plate> _lsidMap; - private final List<Plate> _templates; + private Map<Container, List<Plate>> _containerPlateMap = new HashMap<>(); // internal collection to help un-cache all plates for a container - private PlateCollections(Container c) + @Override + public Plate load(@NotNull String key, @Nullable Object argument) { - Map<Integer, Plate> rowIdMap = new HashMap<>(); - Map<String, Plate> nameMap = new HashMap<>(); - Map<Lsid, Plate> lsidMap = new HashMap<>(); - List<Plate> templates = new ArrayList<>(); + // parse the cache key + PlateCacheKey cacheKey = new PlateCacheKey(key); - new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), SimpleFilter.createContainerFilter(c), null).forEach(PlateImpl.class, plate -> { - PlateManager.get().populatePlate(plate); - - rowIdMap.put(plate.getRowId(), plate); - if (nameMap.containsKey(plate.getName())) - { - LOG.error(String.format("A duplicate Plate name : %s was found in the same folder : %s. We recommend that the duplicate plate(s) are deleted.", plate.getName(), c.getPath())); - } - nameMap.put(plate.getName(), plate); - lsidMap.put(Lsid.parse(plate.getLSID()), plate); - if (plate.isTemplate()) - templates.add(plate); - }); - - templates.sort(Comparator.comparing(Plate::getName)); - - _templates = templates; - _rowIdMap = rowIdMap; - _nameMap = nameMap; - _lsidMap = lsidMap; - } - - private @Nullable Plate getForRowId(int rowId) - { - return _rowIdMap.get(rowId); - } + SimpleFilter filter = SimpleFilter.createContainerFilter(cacheKey._container); + filter.addCondition(FieldKey.fromParts(cacheKey._type.name()), cacheKey._identifier); - private @Nullable Plate getForName(String name) - { - return _nameMap.get(name); - } + List<PlateImpl> plates = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), filter, null).getArrayList(PlateImpl.class); + assert plates.size() <= 1; - private @Nullable Plate getForLsid(Lsid lsid) - { - return _lsidMap.get(lsid); - } + if (plates.size() == 1) + { + PlateImpl plate = plates.get(0); + PlateManager.get().populatePlate(plate); + LOG.info(String.format("Caching plate \"%s\" for folder %s", plate.getName(), cacheKey._container.getPath())); - private @NotNull Collection<Plate> getPlates() - { - return _rowIdMap.values(); + // add all cache keys for this plate + addCacheKeys(cacheKey, plate); + return plate; + } + return null; } - private @NotNull List<Plate> getPlateTemplates() + private void addCacheKeys(PlateCacheKey cacheKey, Plate plate) { - return _templates; + if (plate != null) + { + if (plate.getName() == null) + throw new IllegalArgumentException("Plate cannot be cached, name is null"); + if (plate.getRowId() == null) + throw new IllegalArgumentException("Plate cannot be cached, rowId is null"); + if (plate.getLSID() == null) + throw new IllegalArgumentException("Plate cannot be cached, LSID is null"); + + // add the plate for the other key types + if (cacheKey._type != PlateCacheKey.Type.rowId) + PLATE_CACHE.put(PlateCacheKey.getCacheKey(plate.getContainer(), plate.getRowId()), plate); + if (cacheKey._type != PlateCacheKey.Type.lsid) + PLATE_CACHE.put(PlateCacheKey.getCacheKey(plate.getContainer(), Lsid.parse(plate.getLSID())), plate); + if (cacheKey._type != PlateCacheKey.Type.name) + PLATE_CACHE.put(PlateCacheKey.getCacheKey(plate.getContainer(), plate.getName()), plate); + + _containerPlateMap.computeIfAbsent(cacheKey._container, k -> new ArrayList<>()).add(plate); + } } } - static @Nullable Plate getPlate(Container c, int rowId) + public static @Nullable Plate getPlate(Container c, int rowId) { - return PLATE_COLLECTIONS_CACHE.get(c).getForRowId(rowId); + Plate plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, rowId)); + // We allow plates to be mutated, return a copy of the cached object which still references the + // original wells and well groups + return plate != null ? plate.copy() : null; } - static @Nullable Plate getPlate(Container c, String name) + public static @Nullable Plate getPlate(Container c, String name) { - return PLATE_COLLECTIONS_CACHE.get(c).getForName(name); + Plate plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, name)); + return plate != null ? plate.copy() : null; } - static @Nullable Plate getPlate(Container c, Lsid lsid) + public static @Nullable Plate getPlate(Container c, Lsid lsid) { - return PLATE_COLLECTIONS_CACHE.get(c).getForLsid(lsid); + Plate plate = PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, lsid)); + return plate != null ? plate.copy() : null; } - static @NotNull Collection<Plate> getPlates(Container c) + public static @NotNull Collection<Plate> getPlates(Container c) { - return PLATE_COLLECTIONS_CACHE.get(c).getPlates(); + List<Plate> plates = new ArrayList<>(); + List<Integer> ids = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), + Collections.singleton("RowId"), + SimpleFilter.createContainerFilter(c), null).getArrayList(Integer.class); + for (Integer id : ids) + { + plates.add(PLATE_CACHE.get(PlateCacheKey.getCacheKey(c, id))); + } + return plates; } - static @NotNull List<Plate> getPlateTemplates(Container c) + public static @NotNull List<Plate> getPlateTemplates(Container c) { - return PLATE_COLLECTIONS_CACHE.get(c).getPlateTemplates(); + List<Plate> templates = new ArrayList<>(); + for (Plate plate : getPlates(c)) + { + if (plate.isTemplate()) + templates.add(plate); + } + return templates.stream() + .sorted(Comparator.comparing(Plate::getName)) + .collect(Collectors.toList()); } public static void uncache(Container c) { - PLATE_COLLECTIONS_CACHE.remove(c); + LOG.info(String.format("Clearing cache for folder %s", c.getPath())); + + // uncache all plates for this container + if (_loader._containerPlateMap.containsKey(c)) + { + List<Plate> plates = new ArrayList<>(_loader._containerPlateMap.get(c)); + for (Plate plate : plates) + { + uncache(c, plate); + } + _loader._containerPlateMap.remove(c); + } + } + + public static void uncache(Container c, Plate plate) + { + LOG.info(String.format("Un-caching plate \"%s\" for folder %s", plate.getName(), c.getPath())); + + if (plate.getName() == null) + throw new IllegalArgumentException("Plate cannot be uncached, name is null"); + if (plate.getRowId() == null) + throw new IllegalArgumentException("Plate cannot be uncached, rowId is null"); + if (plate.getLSID() == null) + throw new IllegalArgumentException("Plate cannot be uncached, LSID is null"); + + PLATE_CACHE.remove(PlateCacheKey.getCacheKey(c, plate.getName())); + PLATE_CACHE.remove(PlateCacheKey.getCacheKey(c, plate.getRowId())); + PLATE_CACHE.remove(PlateCacheKey.getCacheKey(c, Lsid.parse(plate.getLSID()))); + + if (_loader._containerPlateMap.containsKey(c)) + _loader._containerPlateMap.get(c).remove(plate); } public static void clearCache() { - PLATE_COLLECTIONS_CACHE.clear(); + PLATE_CACHE.clear(); + } + + private static class PlateCacheKey + { + enum Type + { + rowId, + name, + lsid, + } + private Type _type; + private Container _container; + private Object _identifier; + + PlateCacheKey(String key) + { + JSONObject json = new JSONObject(key); + + _type = json.getEnum(Type.class, "type"); + _container = ContainerManager.getForId(json.getString("container")); + _identifier = json.get("identifier"); + } + + public static String getCacheKey(Container c, String name) + { + return _getCacheKey(c, Type.name, name); + } + + public static String getCacheKey(Container c, Integer plateId) + { + return _getCacheKey(c, Type.rowId, plateId); + } + + public static String getCacheKey(Container c, Lsid lsid) + { + return _getCacheKey(c, Type.lsid, lsid.toString()); + } + + private static String _getCacheKey(Container c, Type type, Object identifier) + { + JSONObject json = new JSONObject(); + json.put("container", c.getId()); + json.put("type", type.name()); + json.put("identifier", identifier); + + return json.toString(); + } } } diff --git a/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java b/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java index e3cc55513d6..8dfa7381403 100644 --- a/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java +++ b/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java @@ -109,7 +109,7 @@ private Lsid fromDocumentId(@NotNull String resourceIdentifier) private @Nullable Plate getPlate(@NotNull String resourceIdentifier) { Lsid lsid = fromDocumentId(resourceIdentifier); - return PlateManager.get().getPlate(lsid.toString()); + return PlateManager.get().getPlate(lsid); } @Override diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index b99287a6c65..246c3e2f511 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -21,6 +21,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateCustomField; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.Position; import org.labkey.api.assay.plate.PositionImpl; @@ -30,6 +31,7 @@ import org.labkey.api.data.Transient; import org.labkey.api.query.QueryRowReference; import org.labkey.api.util.GUID; +import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.ActionURL; import org.labkey.assay.PlateController; @@ -43,7 +45,7 @@ import java.util.Map; @JsonInclude(JsonInclude.Include.NON_NULL) -public class PlateImpl extends PropertySetImpl implements Plate +public class PlateImpl extends PropertySetImpl implements Plate, Cloneable { private String _name; private Integer _rowId; @@ -61,10 +63,12 @@ public class PlateImpl extends PropertySetImpl implements Plate private List<WellGroupImpl> _deletedGroups; private WellImpl[][] _wells; - private Map<Integer, WellImpl> _wellMap; + private Map<Integer, Well> _wellMap; private int _runId; // NO_RUNID means no run yet, well data comes from file, dilution data must be calculated private int _plateNumber; + private List<PlateCustomField> _customFields = Collections.emptyList(); + private Integer _metadataDomainId; public PlateImpl() { @@ -523,9 +527,13 @@ public void setWells(WellImpl[][] wells) } @JsonIgnore - public WellImpl[][] getWells() + @Override + public List<Well> getWells() { - return _wells; + if (_wellMap != null) + return _wellMap.values().stream().toList(); + else + return Collections.emptyList(); } @Override @@ -556,4 +564,39 @@ public int getPlateNumber() { return _plateNumber; } + + @Override + public @NotNull List<PlateCustomField> getCustomFields() + { + return _customFields; + } + + public void setCustomFields(List<PlateCustomField> customFields) + { + _customFields = customFields; + } + + public PlateImpl copy() + { + try + { + return (PlateImpl)super.clone(); + } + catch (CloneNotSupportedException e) + { + throw UnexpectedException.wrap(e); + } + } + + @Nullable + @Override + public Integer getMetadataDomainId() + { + return _metadataDomainId; + } + + public void setMetadataDomainId(Integer metadataDomainId) + { + _metadataDomainId = metadataDomainId; + } } diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 74bd2a7da24..2f81105ad86 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -27,17 +27,23 @@ import org.labkey.api.assay.dilution.DilutionCurve; import org.labkey.api.assay.plate.AbstractPlateTypeHandler; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateCustomField; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateTypeHandler; import org.labkey.api.assay.plate.Position; import org.labkey.api.assay.plate.PositionImpl; import org.labkey.api.assay.plate.Well; +import org.labkey.api.assay.plate.WellCustomField; import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.collections.ArrayListMap; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; import org.labkey.api.data.DbScope; import org.labkey.api.data.ImportAliasable; import org.labkey.api.data.ObjectFactory; +import org.labkey.api.data.Results; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.Sort; @@ -82,9 +88,7 @@ import org.labkey.api.view.ActionURL; import org.labkey.api.webdav.WebdavResource; import org.labkey.assay.TsvAssayProvider; -import org.labkey.assay.plate.model.PlateCustomField; import org.labkey.assay.plate.model.PlateType; -import org.labkey.assay.plate.model.WellCustomField; import org.labkey.assay.plate.model.WellGroupBean; import org.labkey.assay.plate.query.PlateSchema; import org.labkey.assay.plate.query.PlateTable; @@ -114,6 +118,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class PlateManager implements PlateService { @@ -336,16 +341,17 @@ public Plate createPlateTemplate(Container container, String templateType, int r return PlateCache.getPlate(container, lsid); } - /** - * Note that this does not use the cache. - */ - public @Nullable Plate getPlate(String lsid) + public @Nullable Plate getPlate(Lsid lsid) { SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("lsid"), lsid); - PlateImpl plate = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), filter, null).getObject(PlateImpl.class); - populatePlate(plate); - - return plate; + String container = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), Collections.singleton("Container"), filter, null).getObject(String.class); + if (container != null) + { + Container c = ContainerManager.getForId(container); + if (c != null) + return PlateCache.getPlate(c, lsid); + } + return null; } /** @@ -353,10 +359,7 @@ public Plate createPlateTemplate(Container container, String templateType, int r */ public boolean plateExists(Container c, String name) { - SimpleFilter filter = SimpleFilter.createContainerFilter(c); - filter.addCondition(FieldKey.fromParts("Name"), name); - - return new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate(), filter, null).getRowCount() > 0; + return PlateCache.getPlate(c, name) != null; } private Collection<Plate> getPlates(Container c) @@ -396,7 +399,6 @@ public WellGroup getWellGroup(String lsid) return null; } - private void setProperties(Container container, PropertySetImpl propertySet) { Map<String, ObjectProperty> props = OntologyManager.getPropertyObjects(container, propertySet.getLSID()); @@ -476,6 +478,32 @@ protected void populatePlate(PlateImpl plate) for (WellGroupImpl group : sortedGroups) plate.addWellGroup(group); + + // custom plate properties + Domain domain = getPlateMetadataDomain(plate.getContainer(), null); + if (domain != null) + { + plate.setMetadataDomainId(domain.getTypeId()); + SQLFragment sqlPlateProps = new SQLFragment("SELECT PropertyURI FROM ").append(AssayDbSchema.getInstance().getTableInfoPlateProperty(), "PP") + .append(" WHERE PlateId = ?").add(plate.getRowId()); + + List<DomainProperty> fields = new ArrayList<>(); + for (String uri : new SqlSelector(AssayDbSchema.getInstance().getSchema(), sqlPlateProps).getArrayList(String.class)) + { + DomainProperty dp = domain.getPropertyByURI(uri); + if (dp == null) + throw new IllegalArgumentException("Failed to get plate custom field. \"" + uri + "\" does not exist on domain."); + + fields.add(dp); + } + + if (!fields.isEmpty()) + { + plate.setCustomFields(fields.stream() + .sorted(Comparator.comparing(DomainProperty::getName)) + .map(PlateCustomField::new).toList()); + } + } } private WellImpl[] getWells(Plate plate) @@ -540,6 +568,11 @@ private int savePlateImpl(Container container, User user, PlateImpl plate) throw Lsid lsid = Lsid.parse(plateInstanceLsid); plateObjectLsid = lsid.edit().setObjectId(LSID_CLASS_OBJECT_ID).toString(); + // special case if the plate name changes, we want to remove the cache key with the old name + Plate oldPlate = getPlate(container, plateId); + if (!oldPlate.getName().equals(plate.getName())) + clearCache(container, oldPlate); + qus.updateRows(user, container, Collections.singletonList(plateRow), null, errors, null, null); if (errors.hasErrors()) throw errors; @@ -552,6 +585,8 @@ private int savePlateImpl(Container container, User user, PlateImpl plate) throw throw errors; plateId = (Integer)insertedRows.get(0).get("RowId"); plateInstanceLsid = (String)insertedRows.get(0).get("Lsid"); + plate.setRowId(plateId); + plate.setLsid(plateInstanceLsid); } savePropertyBag(container, plateInstanceLsid, plateObjectLsid, plate.getProperties(), updateExisting); @@ -609,9 +644,9 @@ private int savePlateImpl(Container container, User user, PlateImpl plate) throw Map<Pair<Integer, Integer>, PositionImpl> existingPositionMap = new HashMap<>(); if (updateExisting) { - for (PositionImpl existingPosition : getWells(plate)) + for (Well existingPosition : plate.getWells()) { - existingPositionMap.put(Pair.of(existingPosition.getRow(), existingPosition.getCol()), existingPosition); + existingPositionMap.put(Pair.of(existingPosition.getRow(), existingPosition.getColumn()), (PositionImpl) existingPosition); } } else @@ -672,7 +707,7 @@ private int savePlateImpl(Container container, User user, PlateImpl plate) throw final Integer plateRowId = plateId; transaction.addCommitTask(() -> { - clearCache(container); + clearCache(container, plate); indexPlate(container, plateRowId); }, DbScope.CommitTaskOption.POSTCOMMIT); transaction.commit(); @@ -753,10 +788,10 @@ public void deletePlate(Container container, User user, int rowid) throws Except } // Called by the Plate Query Update Service after deleting a plate - public void afterPlateDelete(Container container, Lsid plateLsid) + public void afterPlateDelete(Container container, Plate plate) { - clearCache(container); - deindexPlates(List.of(plateLsid)); + clearCache(container, plate); + deindexPlates(List.of(Lsid.parse(plate.getLSID()))); } // Called by the Plate Query Update Service prior to deleting a plate @@ -764,18 +799,12 @@ public void beforePlateDelete(Container container, Integer plateId) { final AssayDbSchema schema = AssayDbSchema.getInstance(); - SimpleFilter plateFilter = SimpleFilter.createContainerFilter(container); - plateFilter.addCondition(FieldKey.fromParts("RowId"), plateId); - PlateImpl plate = new TableSelector(schema.getTableInfoPlate(), - plateFilter, null).getObject(PlateImpl.class); - WellGroupImpl[] wellgroups = getWellGroups(plate); - WellImpl[] wells = getWells(plate); - + Plate plate = PlateCache.getPlate(container, plateId); List<String> lsids = new ArrayList<>(); lsids.add(plate.getLSID()); - for (WellGroupImpl wellgroup : wellgroups) + for (WellGroup wellgroup : plate.getWellGroups()) lsids.add(wellgroup.getLSID()); - for (WellImpl well : wells) + for (Well well : plate.getWells()) lsids.add(well.getLsid()); SimpleFilter plateIdFilter = SimpleFilter.createContainerFilter(container); @@ -957,7 +986,7 @@ public Plate getObject(Lsid lsid) if (lsid == null) return null; - return PlateManager.get().getPlate(lsid.toString()); + return PlateManager.get().getPlate(lsid); } @Override @@ -1035,10 +1064,10 @@ public void registerLsidHandlers() public Plate copyPlate(Plate source, User user, Container destContainer) throws Exception { - Plate destination = PlateService.get().getPlate(destContainer, source.getName()); + Plate destination = getPlate(destContainer, source.getName()); if (destination != null) throw new PlateService.NameConflictException(source.getName()); - destination = PlateService.get().createPlateTemplate(destContainer, source.getType(), source.getRows(), source.getColumns()); + destination = createPlateTemplate(destContainer, source.getType(), source.getRows(), source.getColumns()); destination.setName(source.getName()); for (String property : source.getPropertyNames()) destination.setProperty(property, source.getProperty(property)); @@ -1052,7 +1081,7 @@ public Plate copyPlate(Plate source, User user, Container destContainer) copyGroup.setProperty(property, originalGroup.getProperty(property)); } save(destContainer, user, destination); - return this.getPlate(destContainer, destination.getName()); + return getPlate(destContainer, destination.getName()); } @Override @@ -1065,6 +1094,11 @@ public void registerPlateTypeHandler(PlateTypeHandler handler) _plateTypeHandlers.put(handler.getAssayType(), handler); } + public void clearCache(Container c, Plate plate) + { + PlateCache.uncache(c, plate); + } + public void clearCache(Container c) { PlateCache.uncache(c); @@ -1214,7 +1248,8 @@ private Container getPlateMetadataDomainContainer(Container container) return container.isProject() ? container : container.getProject(); } - private @NotNull Domain ensurePlateMetadataDomain(Container container, User user) throws ValidationException + @Override + public @NotNull Domain ensurePlateMetadataDomain(Container container, User user) throws ValidationException { Domain vocabDomain = getPlateMetadataDomain(container, user); @@ -1346,46 +1381,46 @@ private Container getPlateMetadataDomainContainer(Container container) DomainProperty dp = domain.getPropertyByURI(field.getPropertyURI()); if (dp == null) throw new IllegalArgumentException("Failed to add plate custom field. \"" + field.getPropertyURI() + "\" does not exist on domain."); - fieldsToAdd.add(dp); } if (!fieldsToAdd.isEmpty()) { - List<String> propertyURIs = fieldsToAdd.stream().map(DomainProperty::getPropertyURI).collect(Collectors.toList()); - - // verify fields aren't already associated with the plate - SQLFragment sql = new SQLFragment("SELECT PropertyURI FROM ").append(AssayDbSchema.getInstance().getTableInfoPlateProperty(), "PP") - .append(" WHERE PlateId = ? ").add(plateId) - .append(" AND PropertyURI ").appendInClause(propertyURIs, AssayDbSchema.getInstance().getSchema().getSqlDialect()); - - List<String> existingProps = new SqlSelector(AssayDbSchema.getInstance().getSchema(), sql).getArrayList(String.class); - if (!existingProps.isEmpty()) + try (DbScope.Transaction transaction = ExperimentService.get().ensureTransaction()) { - throw new IllegalArgumentException("Failed to add plate custom fields. Custom fields \"" + String.join(",", existingProps) + "\" already are associated with this plate."); - } + Set<String> existingProps = plate.getCustomFields().stream().map(PlateCustomField::getPropertyURI).collect(Collectors.toSet()); + for (DomainProperty dp : fieldsToAdd) + { + if (existingProps.contains(dp.getPropertyURI())) + throw new IllegalArgumentException(String.format("Failed to add plate custom fields. Custom field \"%s\" already is associated with this plate.", dp.getName())); + } - List<List<?>> insertedValues = new LinkedList<>(); - for (DomainProperty dp : fieldsToAdd) - { - insertedValues.add(List.of(plateId, - dp.getPropertyId(), - dp.getPropertyURI())); + List<List<?>> insertedValues = new LinkedList<>(); + for (DomainProperty dp : fieldsToAdd) + { + insertedValues.add(List.of(plateId, + dp.getPropertyId(), + dp.getPropertyURI())); + } + String insertSql = "INSERT INTO " + AssayDbSchema.getInstance().getTableInfoPlateProperty() + + " (plateId, propertyId, propertyURI)" + + " VALUES (?, ?, ?)"; + Table.batchExecute(AssayDbSchema.getInstance().getSchema(), insertSql, insertedValues); + + transaction.addCommitTask(() -> clearCache(container, plate), DbScope.CommitTaskOption.POSTCOMMIT); + transaction.commit(); } - String insertSql = "INSERT INTO " + AssayDbSchema.getInstance().getTableInfoPlateProperty() + - " (plateId, propertyId, propertyURI)" + - " VALUES (?, ?, ?)"; - Table.batchExecute(AssayDbSchema.getInstance().getSchema(), insertSql, insertedValues); } - return getFields(container, user, plateId); + return getFields(container, plateId); } - public List<PlateCustomField> getFields(Container container, User user, Integer plateId) + public List<PlateCustomField> getFields(Container container, Integer plateId) { - List<PlateCustomField> fields = _getFields(container, user, plateId).stream().map(PlateCustomField::new).toList(); - return fields.stream() - .sorted(Comparator.comparing(PlateCustomField::getName)) - .collect(Collectors.toList()); + Plate plate = getPlate(container, plateId); + if (plate == null) + throw new IllegalArgumentException("Failed to get plate custom fields. Plate id \"" + plateId + "\" not found."); + + return plate.getCustomFields(); } /** @@ -1458,54 +1493,27 @@ public List<PlateCustomField> removeFields(Container container, User user, Integ if (!fieldsToRemove.isEmpty()) { - List<String> propertyURIs = fieldsToRemove.stream().map(DomainProperty::getPropertyURI).collect(Collectors.toList()); - - SQLFragment sql = new SQLFragment("DELETE FROM ").append(AssayDbSchema.getInstance().getTableInfoPlateProperty(), "PP") - .append(" WHERE PlateId = ? ").add(plateId) - .append(" AND PropertyURI ").appendInClause(propertyURIs, AssayDbSchema.getInstance().getSchema().getSqlDialect()); - - new SqlExecutor(AssayDbSchema.getInstance().getSchema()).execute(sql); - } - return getFields(container, user, plateId); - } - - public List<WellCustomField> setFields(Container container, User user, Integer plateId, Integer wellId, List<WellCustomField> fields) throws ValidationException - { - Plate plate = getPlate(container, plateId); - if (plate == null) - throw new IllegalArgumentException("Failed to set plate custom field values. Plate id \"" + plateId + "\" not found."); - - WellImpl well = (WellImpl) plate.getWell(wellId); - if (well == null) - throw new IllegalArgumentException("Failed to set plate custom field values. Well id \"" + wellId + "\" not found."); - - Domain domain = getPlateMetadataDomain(container, user); - if (domain == null) - throw new IllegalArgumentException("Failed to set plate custom field values. Custom fields domain does not exist. Try creating fields first."); - - try (DbScope.Transaction transaction = ExperimentService.get().ensureTransaction()) - { - // verify the fields have been configured for the plate - Set<String> configuredProps = new HashSet<>(); - SQLFragment sql = new SQLFragment("SELECT PropertyURI FROM ").append(AssayDbSchema.getInstance().getTableInfoPlateProperty(), "") - .append(" WHERE PlateId = ?").add(plateId); - new SqlSelector(AssayDbSchema.getInstance().getSchema(), sql).forEach(String.class, configuredProps::add); - - for (WellCustomField field : fields) + try (DbScope.Transaction transaction = ExperimentService.get().ensureTransaction()) { - if (!configuredProps.contains(field.getPropertyURI())) - throw new IllegalArgumentException("Failed to set plate custom field value. Custom field \"" + field.getPropertyURI() + "\" is not configured for this plate."); + List<String> propertyURIs = fieldsToRemove.stream().map(DomainProperty::getPropertyURI).collect(Collectors.toList()); + Set<String> existingProps = plate.getCustomFields().stream().map(PlateCustomField::getPropertyURI).collect(Collectors.toSet()); + for (DomainProperty dp : fieldsToRemove) + { + if (!existingProps.contains(dp.getPropertyURI())) + throw new IllegalArgumentException(String.format("Failed to remove plate custom fields. Custom field \"%s\" is not currently associated with this plate.", dp.getName())); + } - DomainProperty dp = domain.getPropertyByURI(field.getPropertyURI()); - if (dp == null) - throw new IllegalArgumentException("Failed to set plate custom field values. Custom field \"" + field.getPropertyURI() + "\" not found."); + SQLFragment sql = new SQLFragment("DELETE FROM ").append(AssayDbSchema.getInstance().getTableInfoPlateProperty(), "") + .append(" WHERE PlateId = ? ").add(plateId) + .append(" AND PropertyURI ").appendInClause(propertyURIs, AssayDbSchema.getInstance().getSchema().getSqlDialect()); + + new SqlExecutor(AssayDbSchema.getInstance().getSchema()).execute(sql); - OntologyManager.updateObjectProperty(user, container, dp.getPropertyDescriptor(), well.getLsid(), field.getValue(), null, true); + transaction.addCommitTask(() -> clearCache(container, plate), DbScope.CommitTaskOption.POSTCOMMIT); + transaction.commit(); } - transaction.commit(); } - - return getWellCustomFields(container, user, plateId, wellId); + return getFields(container, plateId); } public static final class TestCase @@ -1669,5 +1677,123 @@ public void testCreatePlateTemplates() throws Exception assertTrue("Expected saved plate to have the template field set to true", template.isTemplate()); } } + + @Test + public void testCreatePlateMetadata() throws Exception + { + final Container c = JunitUtil.getTestContainer(); + final User user = TestContext.get().getUser(); + + PlateService.get().deleteAllPlateData(c); + Domain domain = PlateManager.get().getPlateMetadataDomain(c ,user); + if (domain != null) + domain.delete(user); + + Plate plate = PlateService.get().createPlateTemplate(c, TsvPlateTypeHandler.TYPE, 16, 24); + plate.setName("new plate with metadata"); + int plateId = PlateService.get().save(c, user, plate); + + // Assert + assertTrue("Expected saved plateId to be returned", plateId != 0); + + // create custom properties + List<GWTPropertyDescriptor> customFields = List.of( + new GWTPropertyDescriptor("barcode", "http://www.w3.org/2001/XMLSchema#string"), + new GWTPropertyDescriptor("concentration", "http://www.w3.org/2001/XMLSchema#double"), + new GWTPropertyDescriptor("negativeControl", "http://www.w3.org/2001/XMLSchema#double")); + + List<PlateCustomField> fields = PlateManager.get().createPlateMetadataFields(c, user, customFields); + + // Verify returned sorted by name + assertTrue("Expected plate custom fields", fields.size() == 3); + assertTrue("Expected barcode custom field", fields.get(0).getName().equals("barcode")); + assertTrue("Expected concentration custom field", fields.get(1).getName().equals("concentration")); + assertTrue("Expected negativeControl custom field", fields.get(2).getName().equals("negativeControl")); + + // assign custom fields to the plate + assertTrue("Expected custom fields to be added to the plate", PlateManager.get().addFields(c, user, plateId, fields).size() == 3); + + // verification when adding custom fields to the plate + try + { + PlateManager.get().addFields(c, user, plateId, fields); + fail("Expected a validation error when adding existing fields"); + } + catch (IllegalArgumentException e) + { + assertTrue("Expected validation exception", e.getMessage().equals("Failed to add plate custom fields. Custom field \"barcode\" already is associated with this plate.")); + } + + // remove a plate custom field + fields = PlateManager.get().removeFields(c, user, plateId, List.of(fields.get(0))); + assertTrue("Expected 2 plate custom fields", fields.size() == 2); + assertTrue("Expected concentration custom field", fields.get(0).getName().equals("concentration")); + assertTrue("Expected negativeControl custom field", fields.get(1).getName().equals("negativeControl")); + + // select wells + SimpleFilter filter = SimpleFilter.createContainerFilter(c); + filter.addCondition(FieldKey.fromParts("PlateId"), plateId); + filter.addCondition(FieldKey.fromParts("Row"), 0); + List< org.labkey.assay.plate.model.Well> wells = new TableSelector(AssayDbSchema.getInstance().getTableInfoWell(), filter, new Sort("Col")).getArrayList(org.labkey.assay.plate.model.Well.class); + + assertTrue("Expected 24 wells to be returned", wells.size() == 24); + + // update + TableInfo wellTable = QueryService.get().getUserSchema(user, c, PlateSchema.SCHEMA_NAME).getTable(WellTable.NAME); + QueryUpdateService qus = wellTable.getUpdateService(); + BatchValidationException errors = new BatchValidationException(); + + // verify metadata update works for Property URI as well as field key + org.labkey.assay.plate.model.Well well = wells.get(0); + List<Map<String, Object>> rows = List.of(CaseInsensitiveHashMap.of( + "rowid", well.getRowId(), + fields.get(0).getPropertyURI(), 1.25, // concentration + fields.get(1).getPropertyURI(), 5.25 // negativeControl + )); + + qus.updateRows(user, c, rows, null, errors, null, null); + if (errors.hasErrors()) + fail(errors.getMessage()); + + well = wells.get(1); + rows = List.of(CaseInsensitiveHashMap.of( + "rowid", well.getRowId(), + "properties/concentration", 2.25, + "properties/negativeControl", 6.25 + )); + + qus.updateRows(user, c, rows, null, errors, null, null); + if (errors.hasErrors()) + fail(errors.getMessage()); + + ColumnInfo colConcentration = wellTable.getColumn("properties/concentration"); + ColumnInfo colNegControl = wellTable.getColumn("properties/negativeControl"); + + // verify vocab property updates + try (Results r = QueryService.get().select(wellTable, List.of(colConcentration, colNegControl), filter, new Sort("Col"))) + { + int row = 0; + while (r.next()) + { + if (row == 0) + { + assertEquals(1.25, r.getDouble(colConcentration.getFieldKey()), 0); + assertEquals(5.25, r.getDouble(colNegControl.getFieldKey()), 0); + } + else if (row == 1) + { + assertEquals(2.25, r.getDouble(colConcentration.getFieldKey()), 0); + assertEquals(6.25, r.getDouble(colNegControl.getFieldKey()), 0); + } + else + { + // the remainder should be null + assertEquals(0, r.getDouble(colConcentration.getFieldKey()), 0); + assertEquals(0, r.getDouble(colNegControl.getFieldKey()), 0); + } + row++; + } + } + } } } diff --git a/assay/src/org/labkey/assay/plate/WellGroupImpl.java b/assay/src/org/labkey/assay/plate/WellGroupImpl.java index 0a48dd6656f..d4b195cf4a3 100644 --- a/assay/src/org/labkey/assay/plate/WellGroupImpl.java +++ b/assay/src/org/labkey/assay/plate/WellGroupImpl.java @@ -97,7 +97,6 @@ public WellGroupImpl(PlateImpl plate, WellGroupImpl template) return template.detailsURL(); } - private static List<? extends Position> sortPositions(List<? extends Position> positions) { List<? extends Position> sortedPositions = new ArrayList<>(positions); @@ -312,6 +311,10 @@ public double getMean() @Override public Plate getPlate() { + if (_plate == null && _plateId != null) + { + _plate = (PlateImpl) PlateCache.getPlate(getContainer(), _plateId); + } return _plate; } diff --git a/assay/src/org/labkey/assay/plate/query/PlateTable.java b/assay/src/org/labkey/assay/plate/query/PlateTable.java index 59c59cadefd..96bba362da0 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateTable.java @@ -223,7 +223,6 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI public List<Map<String, Object>> insertRows(User user, Container container, List<Map<String, Object>> rows, BatchValidationException errors, @Nullable Map<Enum, Object> configParameters, Map<String, Object> extraScriptContext) { List<Map<String, Object>> results = super._insertRowsUsingDIB(user, container, rows, getDataIteratorContext(errors, InsertOption.INSERT, configParameters), extraScriptContext); - PlateManager.get().clearCache(container); return results; } @@ -249,7 +248,7 @@ protected Map<String, Object> updateRow(User user, Container container, Map<Stri } Map<String, Object> newRow = super.updateRow(user, container, row, oldRow); - PlateManager.get().clearCache(container); + PlateManager.get().clearCache(container, plate); return newRow; } @@ -267,11 +266,10 @@ protected Map<String, Object> deleteRow(User user, Container container, Map<Stri try (DbScope.Transaction transaction = AssayDbSchema.getInstance().getScope().ensureTransaction()) { - final Lsid plateLsid = new Lsid(plate.getLSID()); PlateManager.get().beforePlateDelete(container, plateId); Map<String, Object> returnMap = super.deleteRow(user, container, oldRowMap); - transaction.addCommitTask(() -> PlateManager.get().afterPlateDelete(container, plateLsid), DbScope.CommitTaskOption.POSTCOMMIT); + transaction.addCommitTask(() -> PlateManager.get().afterPlateDelete(container, plate), DbScope.CommitTaskOption.POSTCOMMIT); transaction.commit(); return returnMap; diff --git a/assay/src/org/labkey/assay/plate/query/WellGroupTable.java b/assay/src/org/labkey/assay/plate/query/WellGroupTable.java index ad7cc853035..478865efd8c 100644 --- a/assay/src/org/labkey/assay/plate/query/WellGroupTable.java +++ b/assay/src/org/labkey/assay/plate/query/WellGroupTable.java @@ -17,6 +17,7 @@ package org.labkey.assay.plate.query; import org.jetbrains.annotations.Nullable; +import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.data.ColumnInfo; @@ -50,6 +51,7 @@ import org.labkey.api.query.SimpleUserSchema; import org.labkey.api.query.UserSchema; import org.labkey.api.security.User; +import org.labkey.assay.plate.PlateCache; import org.labkey.assay.plate.PlateManager; import org.labkey.assay.query.AssayDbSchema; @@ -181,11 +183,19 @@ protected Map<String, Object> deleteRow(User user, Container container, Map<Stri Integer wellGroupId = (Integer)oldRowMap.get("RowId"); if (wellGroupId != null) { - PlateManager.get().beforeDeleteWellGroup(container, wellGroupId); - Map<String, Object> returnMap = super.deleteRow(user, container, oldRowMap); - transaction.commit(); - - return returnMap; + WellGroup wellGroup = PlateManager.get().getWellGroup(container, wellGroupId); + if (wellGroup != null) + { + Plate plate = wellGroup.getPlate(); + PlateManager.get().beforeDeleteWellGroup(container, wellGroupId); + Map<String, Object> returnMap = super.deleteRow(user, container, oldRowMap); + + if (plate != null) + transaction.addCommitTask(() -> PlateCache.uncache(container, plate), DbScope.CommitTaskOption.POSTCOMMIT); + transaction.commit(); + + return returnMap; + } } return Collections.emptyMap(); } diff --git a/assay/src/org/labkey/assay/plate/query/WellTable.java b/assay/src/org/labkey/assay/plate/query/WellTable.java index 9d07383d654..541adaa4699 100644 --- a/assay/src/org/labkey/assay/plate/query/WellTable.java +++ b/assay/src/org/labkey/assay/plate/query/WellTable.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.Well; import org.labkey.api.collections.CaseInsensitiveHashSet; import org.labkey.api.data.ColumnInfo; @@ -17,14 +18,20 @@ import org.labkey.api.dataiterator.StandardDataIteratorBuilder; import org.labkey.api.dataiterator.TableInsertDataIteratorBuilder; import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.OntologyManager; +import org.labkey.api.exp.PropertyColumn; +import org.labkey.api.exp.PropertyDescriptor; import org.labkey.api.exp.property.Domain; +import org.labkey.api.exp.property.DomainProperty; import org.labkey.api.exp.property.PropertyService; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DefaultQueryUpdateService; import org.labkey.api.query.FieldKey; +import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.PropertyForeignKey; import org.labkey.api.query.QueryForeignKey; import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; import org.labkey.api.query.SimpleUserSchema; import org.labkey.api.query.UserSchema; import org.labkey.api.query.ValidationException; @@ -32,7 +39,9 @@ import org.labkey.assay.plate.PlateManager; import org.labkey.assay.query.AssayDbSchema; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,6 +54,7 @@ public class WellTable extends SimpleUserSchema.SimpleTable<UserSchema> public static final String NAME = "Well"; private static final List<FieldKey> defaultVisibleColumns = new ArrayList<>(); private static final Set<String> ignoredColumns = new CaseInsensitiveHashSet(); + private Map<FieldKey, DomainProperty> _vocabularyFieldMap = new HashMap<>(); static { @@ -83,6 +93,13 @@ private void addVocabularyDomains() col.setLabel("Plate Metadata"); col.setDescription("Custom properties associated with the plate well"); } + + for (DomainProperty field : domain.getProperties()) + { + // resolve vocabulary fields by property URI and field key + _vocabularyFieldMap.put(FieldKey.fromParts("properties", field.getName()), field); + _vocabularyFieldMap.put(FieldKey.fromParts(field.getPropertyURI()), field); + } } } @@ -107,6 +124,38 @@ protected boolean acceptColumn(ColumnInfo col) return super.acceptColumn(col) && !ignoredColumns.contains(col.getName()); } + /** + * Override to resolve Property URIs for vocabulary columns during update. Consider adding the + * capability to resolve vocabulary columns by field key or name. + */ + @Override + protected ColumnInfo resolveColumn(String name) + { + ColumnInfo lsidCol = getColumn("LSID", false); + if (lsidCol != null) + { + // Attempt to resolve the column name as a property URI if it looks like a URI + FieldKey fieldKey = FieldKey.decode(name); + if (_vocabularyFieldMap.containsKey(fieldKey)) + { + DomainProperty field = _vocabularyFieldMap.get(fieldKey); + + // mark vocab propURI col as Voc column + PropertyDescriptor pd = OntologyManager.getPropertyDescriptor(field.getPropertyURI(), getContainer()); + if (pd != null) + { + PropertyColumn pc = new PropertyColumn(pd, lsidCol, getContainer(), getUserSchema().getUser(), false); + String label = pc.getLabel(); + pc.setFieldKey(fieldKey); + pc.setLabel(label); + + return pc; + } + } + } + return super.resolveColumn(name); + } + @Override public MutableColumnInfo wrapColumn(ColumnInfo col) { @@ -203,5 +252,22 @@ public List<Map<String, Object>> insertRows(User user, Container container, List { return super._insertRowsUsingDIB(user, container, rows, getDataIteratorContext(errors, InsertOption.INSERT, configParameters), extraScriptContext); } + + @Override + protected Map<String, Object> updateRow(User user, Container container, Map<String, Object> row, @NotNull Map<String, Object> oldRow) throws InvalidKeyException, ValidationException, QueryUpdateServiceException, SQLException + { + // enforce no updates if the plate has been imported in an assay run + if (oldRow.containsKey("plateId")) + { + Plate plate = PlateManager.get().getPlate(container, (Integer)oldRow.get("plateId")); + if (plate != null) + { + int runsInUse = PlateManager.get().getRunCountUsingPlate(container, plate); + if (runsInUse > 0) + throw new QueryUpdateServiceException(String.format("This %s is used by %d runs and its wells cannot be modified.", plate.isTemplate() ? "Plate template" : "Plate", runsInUse)); + } + } + return super.updateRow(user, container, row, oldRow); + } } }