diff --git a/api/src/org/labkey/api/admin/AdminBean.java b/api/src/org/labkey/api/admin/AdminBean.java index 3f3439d60da..ac20220ce9f 100644 --- a/api/src/org/labkey/api/admin/AdminBean.java +++ b/api/src/org/labkey/api/admin/AdminBean.java @@ -29,6 +29,7 @@ import org.labkey.api.security.UserManager; import org.labkey.api.settings.AppProps; import org.labkey.api.util.DateUtil; +import org.labkey.api.util.Formats; import org.labkey.api.util.HtmlString; import org.labkey.api.util.HtmlStringBuilder; import org.labkey.api.util.MothershipReport; @@ -77,6 +78,7 @@ public static class RecentUser public static final String serverGuid = AppProps.getInstance().getServerGUID(); public static final String serverSessionGuid = AppProps.getInstance().getServerSessionGUID(); public static final String servletContainer = ModuleLoader.getServletContext().getServerInfo(); + public static final String sessionTimeout = Formats.commaf0.format(ModuleLoader.getServletContext().getSessionTimeout()); @SuppressWarnings("unused") // Available substitution property, not used directly in code public static final String buildTime = ModuleLoader.getInstance().getCoreModule().getBuildTime(); @SuppressWarnings("unused") // Available substitution property, not used directly in code diff --git a/api/src/org/labkey/api/reader/HTMLDataLoader.java b/api/src/org/labkey/api/reader/HTMLDataLoader.java index a792aa627e7..68538f9285c 100644 --- a/api/src/org/labkey/api/reader/HTMLDataLoader.java +++ b/api/src/org/labkey/api/reader/HTMLDataLoader.java @@ -21,6 +21,7 @@ import org.labkey.api.util.FileType; import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.JSoupUtil; +import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -82,12 +83,19 @@ public boolean isHeaderMatch(@NotNull byte[] header) String s = new String(header, StringUtilsLabKey.DEFAULT_CHARSET); List errors = new ArrayList<>(); - Document doc = JSoupUtil.convertHtmlToDocument(s, true, errors); - if (!errors.isEmpty() || doc == null) - return false; + try + { + Document doc = JSoupUtil.convertHtmlToDocument(s, true, errors); + if (!errors.isEmpty() || doc == null) + return false; - // Look for a html>body>table element - return findTable(doc) != null; + // Look for a html>body>table element + return findTable(doc) != null; + } + catch (DOMException e) + { + return false; + } } }; diff --git a/api/src/org/labkey/api/util/JsonUtil.java b/api/src/org/labkey/api/util/JsonUtil.java index 587c1cc2a85..0da6c5993b9 100644 --- a/api/src/org/labkey/api/util/JsonUtil.java +++ b/api/src/org/labkey/api/util/JsonUtil.java @@ -561,7 +561,10 @@ public void testJavascript() throws Exception { // just verifying current behavior: JSONObject escapes /, ObjectMapper does not escape / assertEquals("{\"key<\\/\":\"<\\/script>\"}", new JSONObject(Map.of("key")).toString()); - assertEquals("{\"key\"}", new ObjectMapper().writeValueAsString(Map.of("key"))); + // This is the default ObjectMapper behavior + //assertEquals("{\"key\"}", new ObjectMapper().writeValueAsString(Map.of("key"))); + // This is the "hacked" ObjectMapper behavior see the fix-up code in CoreModule static + assertEquals("{\"key<\\/\":\"<\\/script>\"}", new ObjectMapper().writeValueAsString(Map.of("key"))); // our ObjectMapper should act like JSONObject.toString() assertEquals("{\"key<\\/\":\"<\\/script>\"}", DEFAULT_MAPPER.writeValueAsString(Map.of("key"))); diff --git a/api/webapp/WEB-INF/web.xml b/api/webapp/WEB-INF/web.xml index 02ceab19a7a..2b90954f52e 100755 --- a/api/webapp/WEB-INF/web.xml +++ b/api/webapp/WEB-INF/web.xml @@ -66,7 +66,6 @@ *.post - / diff --git a/assay/api-src/org/labkey/api/assay/plate/AbstractPlateTypeHandler.java b/assay/api-src/org/labkey/api/assay/plate/AbstractPlateLayoutHandler.java similarity index 54% rename from assay/api-src/org/labkey/api/assay/plate/AbstractPlateTypeHandler.java rename to assay/api-src/org/labkey/api/assay/plate/AbstractPlateLayoutHandler.java index abaa96bf715..77a19b68355 100644 --- a/assay/api-src/org/labkey/api/assay/plate/AbstractPlateTypeHandler.java +++ b/assay/api-src/org/labkey/api/assay/plate/AbstractPlateLayoutHandler.java @@ -18,17 +18,22 @@ import org.labkey.api.data.Container; import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; +import org.labkey.api.util.Pair; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * User: klum * Date: Jun 13, 2012 */ -public abstract class AbstractPlateTypeHandler implements PlateTypeHandler +public abstract class AbstractPlateLayoutHandler implements PlateLayoutHandler { + Map, PlateType> _plateTypeMap; + @Override public void validateTemplate(Container container, User user, Plate template) throws ValidationException { @@ -40,6 +45,28 @@ public Map> getDefaultGroupsForTypes() return Collections.emptyMap(); } + abstract protected List> getSupportedPlateSizes(); + + protected void validatePlateType(PlateType plateType) + { + if (!getSupportedPlateTypes().contains(plateType)) + throw new IllegalStateException("The plate type : " + plateType.getDescription() + " is not supported for this layout handler."); + } + + @Override + public List getSupportedPlateTypes() + { + if (_plateTypeMap == null) + { + _plateTypeMap = new HashMap<>(); + for (PlateType type : PlateService.get().getPlateTypes()) + { + _plateTypeMap.put(new Pair<>(type.getRows(), type.getColumns()), type); + } + } + return getSupportedPlateSizes().stream().filter(size -> _plateTypeMap.containsKey(size)).map(size -> _plateTypeMap.get(size)).collect(Collectors.toList()); + } + @Override public boolean showEditorWarningPanel() { 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 b2f7c0f5339..dd44b2bab97 100644 --- a/assay/api-src/org/labkey/api/assay/plate/Plate.java +++ b/assay/api-src/org/labkey/api/assay/plate/Plate.java @@ -40,6 +40,8 @@ public interface Plate extends PropertySet, Identifiable boolean isTemplate(); + @NotNull PlateType getPlateTypeObject(); + @Nullable PlateSet getPlateSetObject(); /** @@ -76,7 +78,7 @@ public interface Plate extends PropertySet, Identifiable int getWellGroupCount(WellGroup.Type type); - String getType(); + String getAssayType(); @Override @Nullable ActionURL detailsURL(); diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateTypeHandler.java b/assay/api-src/org/labkey/api/assay/plate/PlateLayoutHandler.java similarity index 83% rename from assay/api-src/org/labkey/api/assay/plate/PlateTypeHandler.java rename to assay/api-src/org/labkey/api/assay/plate/PlateLayoutHandler.java index 949b7f7a647..1f3055f7f23 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateTypeHandler.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateLayoutHandler.java @@ -16,33 +16,33 @@ package org.labkey.api.assay.plate; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; import org.labkey.api.query.ValidationException; import org.labkey.api.security.User; -import org.labkey.api.util.Pair; -import java.util.List; import java.sql.SQLException; +import java.util.List; import java.util.Map; /** - * User: jeckels - * Date: Apr 23, 2007 + * Represents a handler that can create assay specific plate layouts */ -public interface PlateTypeHandler +public interface PlateLayoutHandler { + @NotNull String getAssayType(); - List getTemplateTypes(Pair size); + @NotNull List getLayoutTypes(PlateType plateType); /** * createTemplate will be given a null value for templateTypeName when it is creating a new template which is a * default for that assay type. */ - Plate createTemplate(@Nullable String templateTypeName, Container container, int rowCount, int colCount) throws SQLException; + Plate createTemplate(@Nullable String templateTypeName, Container container, @NotNull PlateType plateType) throws SQLException; - List> getSupportedPlateSizes(); + List getSupportedPlateTypes(); List getWellGroupTypes(); 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 9a065b1fd95..24b9664c09e 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateService.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateService.java @@ -88,23 +88,21 @@ static PlateService get() * Creates a new plate * @param container The template's container. * @param templateType The type of plate, if associated with a particular assay. - * @param rowCount The number of columns in the plate. - * @param columnCount The number of rows in the plate. + * @param plateType Specifies the overall shape of the plate * @return A newly created plate instance * @throws IllegalArgumentException Thrown if a template of the specified name already exists in the container. */ - @NotNull Plate createPlate(Container container, String templateType, int rowCount, int columnCount); + @NotNull Plate createPlate(Container container, String templateType, @NotNull PlateType plateType); /** * Creates a new plate template. * @param container The template's container. * @param templateType The type of plate template, if associated with a particular assay. - * @param rowCount The number of columns in the plate. - * @param columnCount The number of rows in the plate. + * @param plateType Specifies the overall shape of the plate * @return A newly created plate template instance. * @throws IllegalArgumentException Thrown if a template of the specified name already exists in the container. */ - @NotNull Plate createPlateTemplate(Container container, String templateType, int rowCount, int columnCount); + @NotNull Plate createPlateTemplate(Container container, String templateType, @NotNull PlateType plateType); /** * Adds a new well group to the plate @@ -164,6 +162,17 @@ static PlateService get() */ @Nullable PlateSet getPlateSet(Container container, int plateSetId); + /** + * Returns the list of available plate types. + * @return + */ + @NotNull List getPlateTypes(); + + /** + * Returns the plate type matching the specified shape. + */ + @Nullable PlateType getPlateType(int rows, int columns); + /** * Returns the number of assay runs that are linked to the specified plate. Currently, this only works * for the standard assay with plate support since other assays types do not store the plate ID with the @@ -235,7 +244,7 @@ static PlateService get() /** * Registers a handler for a particular type of plate */ - void registerPlateTypeHandler(PlateTypeHandler handler); + void registerPlateLayoutHandler(PlateLayoutHandler handler); /** * Calculates a dilution curve for the specified well groups. diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java index 348b0779248..d59e3e2a31c 100644 --- a/assay/api-src/org/labkey/api/assay/plate/PlateSet.java +++ b/assay/api-src/org/labkey/api/assay/plate/PlateSet.java @@ -15,6 +15,8 @@ public interface PlateSet String getName(); + String getPlateSetId(); + boolean isArchived(); List getPlates(User user); diff --git a/assay/api-src/org/labkey/api/assay/plate/PlateType.java b/assay/api-src/org/labkey/api/assay/plate/PlateType.java new file mode 100644 index 00000000000..eb4c89cbd76 --- /dev/null +++ b/assay/api-src/org/labkey/api/assay/plate/PlateType.java @@ -0,0 +1,9 @@ +package org.labkey.api.assay.plate; + +public interface PlateType +{ + Integer getRowId(); + String getDescription(); + Integer getRows(); + Integer getColumns(); +} diff --git a/assay/resources/schemas/assay.xml b/assay/resources/schemas/assay.xml index e92a7890cd5..717525b9048 100644 --- a/assay/resources/schemas/assay.xml +++ b/assay/resources/schemas/assay.xml @@ -29,6 +29,11 @@ The unique admin-provided name of each plate template (""NAb: 5 specimens in duplicate", for example). + + + Plate ID + false + The Plate Set that this plate is assigned to. @@ -37,6 +42,14 @@ assay + + The plate type of this plate. + + RowId + PlateType + assay + + int Created By @@ -79,20 +92,12 @@ Boolean indicating whether each plate is a template versus an uploaded instance of a plate template. false - - The number of rows in each plate. - false - - - The number of columns in each plate. - false - false A unique text identifier (a GUID) for the data file associated with each plate. - - A text label of the plate type ("NAb", for example). + + A text label of the plate assay type ("NAb", for example). @@ -178,6 +183,11 @@ + + + Plate Set ID + false + @@ -186,4 +196,16 @@ + + Contains one row per plate type. + + + true + + + + + + +
diff --git a/assay/resources/schemas/dbscripts/postgresql/assay-24.001-24.002.sql b/assay/resources/schemas/dbscripts/postgresql/assay-24.001-24.002.sql new file mode 100644 index 00000000000..371e9cbbca9 --- /dev/null +++ b/assay/resources/schemas/dbscripts/postgresql/assay-24.001-24.002.sql @@ -0,0 +1,61 @@ +CREATE TABLE assay.PlateType +( + RowId SERIAL, + Rows INT NOT NULL, + Columns INT NOT NULL, + Description VARCHAR(300) NOT NULL, + Archived BOOLEAN NOT NULL DEFAULT FALSE, + + CONSTRAINT PK_PlateType PRIMARY KEY (RowId), + CONSTRAINT UQ_PlateType_Rows_Cols UNIQUE (Rows, Columns) +); + +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (3, 4, '12 well (3x4)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (4, 6, '24 well (4x6)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (6, 8, '48 well (6x8)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (8, 12, '96 well (8x12)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (16, 24, '384 well (16x24)'); +INSERT INTO assay.PlateType (Rows, Columns, Description, Archived) VALUES (32, 48, '1536 well (32x48)', TRUE); +INSERT INTO assay.PlateType (Rows, Columns, Description, Archived) VALUES (0, 0, 'Invalid Plate Type (Plates which were created with non-valid row & column combinations)', TRUE); + +-- Rename type column to assayType +ALTER TABLE assay.Plate RENAME COLUMN Type TO AssayType; +-- Add plateType as a FK to assay.PlateType +ALTER TABLE assay.Plate ADD COLUMN PlateType INTEGER; +ALTER TABLE assay.Plate ADD CONSTRAINT FK_Plate_PlateType FOREIGN KEY (PlateType) REFERENCES assay.PlateType (RowId); + +-- Add ID and description columns to Plate and PlateSet tables +ALTER TABLE assay.Plate ADD COLUMN PlateId VARCHAR(200); +ALTER TABLE assay.Plate ADD COLUMN Description VARCHAR(300); +ALTER TABLE assay.PlateSet ADD COLUMN PlateSetId VARCHAR(200); +ALTER TABLE assay.PlateSet ADD COLUMN Description VARCHAR(300); + +-- Most existing plate sets will have a generated name, but mutated ones will get fixed up by the java upgrade script +UPDATE assay.PlateSet SET PlateSetId = Name; + +UPDATE assay.Plate +SET PlateType = + CASE + WHEN (Rows = 3 AND Columns = 4) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 3 AND Columns = 4) + WHEN (Rows = 4 AND Columns = 6) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 4 AND Columns = 6) + WHEN (Rows = 6 AND Columns = 8) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 6 AND Columns = 8) + WHEN (Rows = 8 AND Columns = 12) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 8 AND Columns = 12) + WHEN (Rows = 16 AND Columns = 24) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 16 AND Columns = 24) + WHEN (Rows = 32 AND Columns = 48) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 32 AND Columns = 48) + ELSE (SELECT RowId FROM assay.PlateType WHERE Rows = 0 AND Columns = 0) + END +WHERE PlateType IS NULL; + +ALTER TABLE assay.Plate ALTER COLUMN PlateType SET NOT NULL; +ALTER TABLE assay.Plate DROP COLUMN Rows; +ALTER TABLE assay.Plate DROP COLUMN Columns; + +-- upgrade script to initialize plate and plateSet IDs +SELECT core.executeJavaUpgradeCode('initializePlateAndPlateSetIDs'); + +-- finalize plate and plateSet ID columns +ALTER TABLE assay.Plate ALTER COLUMN PlateId SET NOT NULL; +ALTER TABLE assay.Plate ADD CONSTRAINT UQ_Plate_PlateId UNIQUE (PlateId); + +ALTER TABLE assay.PlateSet ALTER COLUMN PlateSetId SET NOT NULL; +ALTER TABLE assay.PlateSet ADD CONSTRAINT UQ_PlateSet_PlateSetId UNIQUE (PlateSetId); diff --git a/assay/resources/schemas/dbscripts/sqlserver/assay-24.001-24.002.sql b/assay/resources/schemas/dbscripts/sqlserver/assay-24.001-24.002.sql new file mode 100644 index 00000000000..7bc1ade63be --- /dev/null +++ b/assay/resources/schemas/dbscripts/sqlserver/assay-24.001-24.002.sql @@ -0,0 +1,63 @@ +CREATE TABLE assay.PlateType +( + RowId INT IDENTITY(1,1), + Rows INT NOT NULL, + Columns INT NOT NULL, + Description NVARCHAR(300) NOT NULL, + Archived BIT NOT NULL DEFAULT 0, + + CONSTRAINT PK_PlateType PRIMARY KEY (RowId), + CONSTRAINT UQ_PlateType_Rows_Cols UNIQUE (Rows, Columns) +); + +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (3, 4, '12 well (3x4)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (4, 6, '24 well (4x6)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (6, 8, '48 well (6x8)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (8, 12, '96 well (8x12)'); +INSERT INTO assay.PlateType (Rows, Columns, Description) VALUES (16, 24, '384 well (16x24)'); +INSERT INTO assay.PlateType (Rows, Columns, Description, Archived) VALUES (32, 48, '1536 well (32x48)', 1); +INSERT INTO assay.PlateType (Rows, Columns, Description, Archived) VALUES (0, 0, 'Invalid Plate Type (Plates which were created with non-valid row & column combinations)', 1); + +-- Rename type column to assayType +EXEC sp_rename 'assay.Plate.Type', 'AssayType', 'COLUMN'; +-- Add type as a FK to assay.PlateType +ALTER TABLE assay.Plate ADD PlateType INT; +GO +ALTER TABLE assay.Plate ADD CONSTRAINT FK_Plate_PlateType FOREIGN KEY (PlateType) REFERENCES assay.PlateType (RowId); + +-- Add ID and description columns to Plate and PlateSet tables +ALTER TABLE assay.Plate ADD PlateId NVARCHAR(200); +ALTER TABLE assay.Plate ADD Description NVARCHAR(300); +ALTER TABLE assay.PlateSet ADD PlateSetId NVARCHAR(200); +ALTER TABLE assay.PlateSet ADD Description NVARCHAR(300); +GO + +-- Most existing plate sets will have a generated name, but mutated ones will get fixed up by the java upgrade script +UPDATE assay.PlateSet SET PlateSetId = Name; + +UPDATE assay.Plate +SET PlateType = + CASE + WHEN (Rows = 3 AND Columns = 4) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 3 AND Columns = 4) + WHEN (Rows = 4 AND Columns = 6) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 4 AND Columns = 6) + WHEN (Rows = 6 AND Columns = 8) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 6 AND Columns = 8) + WHEN (Rows = 8 AND Columns = 12) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 8 AND Columns = 12) + WHEN (Rows = 16 AND Columns = 24) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 16 AND Columns = 24) + WHEN (Rows = 32 AND Columns = 48) THEN (SELECT RowId FROM assay.PlateType WHERE Rows = 32 AND Columns = 48) + ELSE (SELECT RowId FROM assay.PlateType WHERE Rows = 0 AND Columns = 0) + END +WHERE PlateType IS NULL; + +ALTER TABLE assay.Plate ALTER COLUMN PlateType INT NOT NULL; +ALTER TABLE assay.Plate DROP COLUMN Rows; +ALTER TABLE assay.Plate DROP COLUMN Columns; + +-- upgrade script to set the plate ID value in assay.Plate +EXEC core.executeJavaUpgradeCode 'initializePlateAndPlateSetIDs'; + +-- finalize plate and plateSet ID columns +ALTER TABLE assay.Plate ALTER COLUMN PlateId NVARCHAR(200) NOT NULL; +ALTER TABLE assay.Plate ADD CONSTRAINT UQ_Plate_PlateId UNIQUE (PlateId); + +ALTER TABLE assay.PlateSet ALTER COLUMN PlateSetId NVARCHAR(200) NOT NULL; +ALTER TABLE assay.PlateSet ADD CONSTRAINT UQ_PlateSet_PlateSetId UNIQUE (PlateSetId); diff --git a/assay/src/org/labkey/assay/AssayModule.java b/assay/src/org/labkey/assay/AssayModule.java index 8906d021081..08d8565b86e 100644 --- a/assay/src/org/labkey/assay/AssayModule.java +++ b/assay/src/org/labkey/assay/AssayModule.java @@ -70,7 +70,7 @@ import org.labkey.assay.plate.AssayPlateMetadataServiceImpl; import org.labkey.assay.plate.PlateDocumentProvider; import org.labkey.assay.plate.PlateManager; -import org.labkey.assay.plate.TsvPlateTypeHandler; +import org.labkey.assay.plate.TsvPlateLayoutHandler; import org.labkey.assay.plate.query.PlateSchema; import org.labkey.assay.query.AssayDbSchema; import org.labkey.assay.query.AssaySchemaImpl; @@ -107,7 +107,7 @@ public String getName() @Override public Double getSchemaVersion() { - return 24.001; + return 24.002; } @Override @@ -149,7 +149,7 @@ protected void init() ExperimentService.get().registerExperimentDataHandler(new PlateMetadataDataHandler()); AssayPlateMetadataService.registerService(PlateMetadataDataHandler.DATA_TYPE, new AssayPlateMetadataServiceImpl()); PropertyService.get().registerDomainKind(new AssayPlateDataDomainKind()); - PlateService.get().registerPlateTypeHandler(new TsvPlateTypeHandler()); + PlateService.get().registerPlateLayoutHandler(new TsvPlateLayoutHandler()); PropertyService.get().registerDomainKind(new DefaultAssayDomainKind()); PropertyService.get().registerDomainKind(new AssayBatchDomainKind()); diff --git a/assay/src/org/labkey/assay/AssayUpgradeCode.java b/assay/src/org/labkey/assay/AssayUpgradeCode.java index 1e398324d6b..1341dc0b954 100644 --- a/assay/src/org/labkey/assay/AssayUpgradeCode.java +++ b/assay/src/org/labkey/assay/AssayUpgradeCode.java @@ -21,6 +21,7 @@ import org.labkey.api.data.NameGenerator; import org.labkey.api.data.ObjectFactory; import org.labkey.api.data.PropertyStorageSpec; +import org.labkey.api.data.Results; import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SchemaTableInfo; import org.labkey.api.data.SqlExecutor; @@ -83,7 +84,7 @@ public static void addAssayDataCreatedColumns(final ModuleContext context) { AssayProvider provider = AssayService.get().getProvider(protocol); if (provider == null) - continue;; + continue; Domain resultsDomain = provider.getResultsDomain(protocol); if (null == resultsDomain || null == resultsDomain.getStorageTableName() || null == resultsDomain.getTypeURI()) @@ -157,7 +158,7 @@ private static void _ensureColumn(String colName, Domain domain, ExpProtocol pro /** * Called from assay-23.002-23.003.sql - * + *

* Switch from storing the protocol plate template by name to the plate lsid. */ @DeferredUpgrade @@ -209,7 +210,7 @@ private static Plate getPlate(ExpProtocol protocol) /** * Called from assay-24.000-24.001.sql - * + *

* The referenced upgrade script creates a new plate set for every plate in the system. We now * want to iterate over each plate set to set the name using the configured name expression. */ @@ -254,4 +255,84 @@ public static void updatePlateSetNames(ModuleContext ctx) throws Exception tx.commit(); } } + + /** + * Called from assay-24.001-24.002.sql + *

+ * Iterate over each plate and plate set to generate a Plate ID and PlateSet ID based on the + * configured name expression for each. + */ + public static void initializePlateAndPlateSetIDs(ModuleContext ctx) throws Exception + { + if (ctx.isNewInstall()) + return; + + DbScope scope = AssayDbSchema.getInstance().getSchema().getScope(); + try (DbScope.Transaction tx = scope.ensureTransaction()) + { + _log.info("Start initializing Plate IDs"); + + try (Results rs = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlate()).getResults()) + { + int platesUpgraded = 0; + while (rs.next()) + { + Map row = rs.getRowMap(); + // get the plate container + String containerId = String.valueOf(row.get("container")); + Container c = ContainerManager.getForId(containerId); + if (c != null) + { + row.put("name", null); + + NameGenerator nameGenerator = new NameGenerator(PlateManager.get().getPlateNameExpression(), AssayDbSchema.getInstance().getTableInfoPlate(), false, c, null, null); + NameGenerator.State state = nameGenerator.createState(false); + String name = nameGenerator.generateName(state, row); + state.cleanUp(); + + SQLFragment sql = new SQLFragment("UPDATE ").append(AssayDbSchema.getInstance().getTableInfoPlate(), "") + .append(" SET PlateId = ?") + .add(name) + .append(" WHERE RowId = ?") + .add(row.get("rowId")); + new SqlExecutor(AssayDbSchema.getInstance().getSchema()).execute(sql); + platesUpgraded++; + } + else + _log.error("Container for Plate ID : " + row.get("rowId") + " could not be resolved."); + } + _log.info("Successfully updated " + platesUpgraded + " plate IDs"); + } + + _log.info("Start initializing PlateSet IDs"); + try (Results rs = new TableSelector(AssayDbSchema.getInstance().getTableInfoPlateSet()).getResults()) + { + NameGenerator nameGenerator = new NameGenerator(PlateManager.get().getPlateSetNameExpression(), AssayDbSchema.getInstance().getTableInfoPlateSet(), false, null, null, null); + NameGenerator.State state = nameGenerator.createState(false); + int plateSetsUpgraded = 0; + while (rs.next()) + { + Map row = rs.getRowMap(); + // for plate sets, they should have a valid PlateSetId, but if the name was not generated (or mutated), regenerate a new + // plate set id + if (!String.valueOf(row.get("name")).startsWith("PLS-")) + { + row.put("name", null); + String name = nameGenerator.generateName(state, row); + state.cleanUp(); + + SQLFragment sql = new SQLFragment("UPDATE ").append(AssayDbSchema.getInstance().getTableInfoPlateSet(), "") + .append(" SET PlateSetId = ?") + .add(name) + .append(" WHERE RowId = ?") + .add(row.get("rowId")); + new SqlExecutor(AssayDbSchema.getInstance().getSchema()).execute(sql); + plateSetsUpgraded++; + } + } + _log.info("Successfully updated " + plateSetsUpgraded + " plate set IDs"); + } + tx.commit(); + } + } } diff --git a/assay/src/org/labkey/assay/PlateController.java b/assay/src/org/labkey/assay/PlateController.java index cf03a5529b6..06f828cc2a7 100644 --- a/assay/src/org/labkey/assay/PlateController.java +++ b/assay/src/org/labkey/assay/PlateController.java @@ -16,7 +16,6 @@ package org.labkey.assay; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.JSONArray; @@ -35,6 +34,7 @@ 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.PlateType; import org.labkey.api.assay.security.DesignAssayPermission; import org.labkey.api.collections.RowMapFactory; import org.labkey.api.data.Container; @@ -66,7 +66,6 @@ import org.labkey.assay.plate.PlateManager; import org.labkey.assay.plate.PlateSetImpl; import org.labkey.assay.plate.PlateUrls; -import org.labkey.assay.plate.model.PlateType; import org.labkey.assay.view.AssayGWTView; import org.springframework.validation.BindException; import org.springframework.validation.Errors; @@ -546,8 +545,8 @@ public void setPlateId(Integer plateId) public static class CreatePlateForm implements ApiJsonForm { private String _name; + private Integer _plateType; private Integer _plateSetId; - private PlateType _plateType; private List> _data = new ArrayList<>(); public String getName() @@ -560,7 +559,7 @@ public Integer getPlateSetId() return _plateSetId; } - public PlateType getPlateType() + public Integer getPlateType() { return _plateType; } @@ -582,7 +581,7 @@ public void bindJson(JSONObject json) _plateSetId = json.getInt("plateSetId"); if (json.has("plateType")) - _plateType = objectMapper.convertValue(json.getJSONObject("plateType"), PlateType.class); + _plateType = json.getInt("plateType"); if (json.has("data")) { @@ -606,11 +605,17 @@ public void bindJson(JSONObject json) @RequiresAnyOf({InsertPermission.class, DesignAssayPermission.class}) public static class CreatePlateAction extends MutatingApiAction { + private PlateType _plateType; + @Override public void validateForm(CreatePlateForm form, Errors errors) { if (form.getPlateType() == null) errors.reject(ERROR_REQUIRED, "Plate \"plateType\" is required."); + + _plateType = PlateManager.get().getPlateType(form.getPlateType()); + if (_plateType == null) + errors.reject(ERROR_REQUIRED, "Plate type id \"" + form.getPlateType() + "\" is invalid."); } @Override @@ -618,7 +623,7 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti { try { - Plate plate = PlateManager.get().createAndSavePlate(getContainer(), getUser(), form.getPlateType(), form.getName(), form.getPlateSetId(), form.getData()); + Plate plate = PlateManager.get().createAndSavePlate(getContainer(), getUser(), _plateType, form.getName(), form.getPlateSetId(), null, form.getData()); return success(plate); } catch (Exception e) @@ -630,16 +635,6 @@ public Object execute(CreatePlateForm form, BindException errors) throws Excepti } } - @RequiresAnyOf({ReadPermission.class, DesignAssayPermission.class}) - public static class GetPlateTypesAction extends ReadOnlyApiAction - { - @Override - public Object execute(Object o, BindException errors) throws Exception - { - return PlateManager.get().getPlateTypes(); - } - } - @RequiresPermission(ReadPermission.class) public static class GetPlateOperationConfirmationDataAction extends ReadOnlyApiAction { @@ -863,47 +858,44 @@ public Object execute(GetPlateForm form, BindException errors) throws Exception } } - // TODO: It'd be nice if we could not have to implement ApiJsonForm here and just bind via Jackson - public static class CreatePlateSetForm implements ApiJsonForm + public static class CreatePlateSetForm { + private String _description; private String _name; - private List _plateTypes = new ArrayList<>(); + private List _plates = new ArrayList<>(); - public String getName() + public String getDescription() { - return _name; + return _description; } - // TODO: Would be really nice to be able to reference plate types by a single identifier. Maybe we just give - // them hard-coded names that can act as a "PK" which we can specify. - public List getPlateTypes() + public void setDescription(String description) { - return _plateTypes; + _description = description; } - @Override - public void bindJson(JSONObject json) + public String getName() { - ObjectMapper objectMapper = JsonUtil.DEFAULT_MAPPER; + return _name; + } - if (json.has("name")) - _name = json.getString("name"); + public void setName(String name) + { + _name = name; + } - if (json.has("plateTypes")) - { - JSONArray plateTypes = json.getJSONArray("plateTypes"); + public List getPlates() + { + return _plates; + } - for (int i = 0; i < plateTypes.length(); i++) - { - JSONObject jsonObj = plateTypes.getJSONObject(i); - if (jsonObj != null) - _plateTypes.add(objectMapper.convertValue(jsonObj, PlateType.class)); - } - } + public void setPlates(List plates) + { + _plates = plates; } } - @Marshal(Marshaller.JSONObject) + @Marshal(Marshaller.Jackson) @RequiresPermission(InsertPermission.class) public static class CreatePlateSetAction extends MutatingApiAction { @@ -913,9 +905,10 @@ public Object execute(CreatePlateSetForm form, BindException errors) throws Exce try { PlateSetImpl plateSet = new PlateSetImpl(); + plateSet.setDescription(form.getDescription()); plateSet.setName(form.getName()); - plateSet = PlateManager.get().createPlateSet(getContainer(), getUser(), plateSet, form.getPlateTypes()); + plateSet = PlateManager.get().createPlateSet(getContainer(), getUser(), plateSet, form.getPlates()); return success(plateSet); } catch (Exception e) diff --git a/assay/src/org/labkey/assay/TsvProviderSchema.java b/assay/src/org/labkey/assay/TsvProviderSchema.java index a48058a8875..ffd84cb5ef0 100644 --- a/assay/src/org/labkey/assay/TsvProviderSchema.java +++ b/assay/src/org/labkey/assay/TsvProviderSchema.java @@ -9,7 +9,7 @@ import org.labkey.api.query.FieldKey; import org.labkey.api.query.SimpleUserSchema; import org.labkey.api.security.User; -import org.labkey.assay.plate.TsvPlateTypeHandler; +import org.labkey.assay.plate.TsvPlateLayoutHandler; import org.labkey.assay.query.AssayDbSchema; import java.util.Collections; @@ -49,7 +49,7 @@ public PlateTemplateTable(TsvProviderSchema schema, ContainerFilter cf) setName(PLATE_TEMPLATE_TABLE); setTitleColumn("Name"); - addCondition(new SimpleFilter(FieldKey.fromParts("Type"), TsvPlateTypeHandler.TYPE)); + addCondition(new SimpleFilter(FieldKey.fromParts("AssayType"), TsvPlateLayoutHandler.TYPE)); } @Override diff --git a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java index 9b7a2c78d9b..93013be7761 100644 --- a/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateDataServiceImpl.java @@ -23,8 +23,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateLayoutHandler; import org.labkey.api.assay.plate.PlateService; -import org.labkey.api.assay.plate.PlateTypeHandler; +import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.plate.Position; import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.gwt.server.BaseRemoteService; @@ -60,7 +61,7 @@ public GWTPlate getTemplateDefinition(String templateName, int plateId, String a try { Plate template; - PlateTypeHandler handler; + PlateLayoutHandler handler; if (templateName != null) { @@ -69,18 +70,21 @@ public GWTPlate getTemplateDefinition(String templateName, int plateId, String a if (template == null) throw new Exception("Plate " + templateName + " does not exist."); - handler = PlateManager.get().getPlateTypeHandler(template.getType()); + handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); if (handler == null) - throw new Exception("Plate template type " + template.getType() + " does not exist."); + throw new Exception("Plate template type " + template.getAssayType() + " does not exist."); } else { // new default template - handler = PlateManager.get().getPlateTypeHandler(assayTypeName); + handler = PlateManager.get().getPlateLayoutHandler(assayTypeName); if (handler == null) throw new Exception("Plate template type " + assayTypeName + " does not exist."); - template = handler.createTemplate(templateTypeName, getContainer(), rowCount, columnCount); + PlateType plateType = PlateService.get().getPlateType(rowCount, columnCount); + if (plateType == null) + throw new Exception("The plate type : (" + rowCount + " x " + columnCount + ") does not exist"); + template = handler.createTemplate(templateTypeName, getContainer(), plateType); } // Translate PlateTemplate to GWTPlate @@ -107,7 +111,7 @@ public GWTPlate getTemplateDefinition(String templateName, int plateId, String a int newPlateId = copyTemplate || template.getRowId() == null ? -1 : template.getRowId(); GWTPlate plate = new GWTPlate(newPlateId, - template.getName(), template.getType(), template.getRows(), + template.getName(), template.getAssayType(), template.getRows(), template.getColumns(), getTypeList(template), handler.showEditorWarningPanel()); plate.setGroups(translated); plate.setTypesToDefaultGroups(handler.getDefaultGroupsForTypes()); @@ -133,7 +137,7 @@ private List getTypeList(Plate template) WellGroup.Type.CONTROL, WellGroup.Type.SPECIMEN, WellGroup.Type.REPLICATE, WellGroup.Type.OTHER); - PlateTypeHandler handler = PlateManager.get().getPlateTypeHandler(template.getType()); + PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(template.getAssayType()); if (handler != null) wellTypes = handler.getWellGroupTypes(); @@ -160,8 +164,8 @@ public int saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exce if (PlateManager.get().plateExists(getContainer(), gwtPlate.getName()) && !replaceIfExisting) throw new Exception("A plate template with name '" + gwtPlate.getName() + "' already exists."); - if (!template.getType().equals(gwtPlate.getType())) - throw new Exception("Plate template type '" + template.getType() + "' cannot be changed for '" + gwtPlate.getName() + "'"); + if (!template.getAssayType().equals(gwtPlate.getType())) + throw new Exception("Plate template type '" + template.getAssayType() + "' cannot be changed for '" + gwtPlate.getName() + "'"); if (template.getRows() != gwtPlate.getRows() || template.getColumns() != gwtPlate.getCols()) throw new Exception("Plate template dimensions cannot be changed for '" + gwtPlate.getName() + "'"); @@ -183,7 +187,10 @@ public int saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exce PlateService.get().deletePlate(getContainer(), getUser(), other.getRowId()); } - template = PlateManager.get().createPlateTemplate(getContainer(), gwtPlate.getType(), gwtPlate.getRows(), gwtPlate.getCols()); + PlateType plateType = PlateService.get().getPlateType(gwtPlate.getRows(), gwtPlate.getCols()); + if (plateType == null) + throw new Exception("The plate type : (" + gwtPlate.getRows() + " x " + gwtPlate.getCols() + ") does not exist"); + template = PlateManager.get().createPlateTemplate(getContainer(), gwtPlate.getType(), plateType); } template.setName(gwtPlate.getName()); @@ -229,7 +236,7 @@ public int saveChanges(GWTPlate gwtPlate, boolean replaceIfExisting) throws Exce group.setProperties(gwtGroup.getProperties()); } - PlateManager.get().getPlateTypeHandler(template.getType()).validateTemplate(getContainer(), getUser(), template); + PlateManager.get().getPlateLayoutHandler(template.getAssayType()).validateTemplate(getContainer(), getUser(), template); return PlateService.get().save(getContainer(), getUser(), template); } catch (BatchValidationException | ValidationException e) diff --git a/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java b/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java index 8dfa7381403..165d68896a6 100644 --- a/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java +++ b/assay/src/org/labkey/assay/plate/PlateDocumentProvider.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; import org.json.JSONObject; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateType; import org.labkey.api.data.Container; import org.labkey.api.exp.Lsid; import org.labkey.api.search.SearchService; @@ -15,7 +16,6 @@ import org.labkey.api.view.ActionURL; import org.labkey.api.webdav.SimpleDocumentResource; import org.labkey.api.webdav.WebdavResource; -import org.labkey.assay.plate.model.PlateType; import java.util.Collection; import java.util.Date; @@ -73,7 +73,7 @@ public static WebdavResource createDocument(@NotNull Plate plate) StringBuilder body = new StringBuilder(); - PlateType plateType = PlateManager.get().getPlateType(plate); + PlateType plateType = plate.getPlateTypeObject(); if (plateType != null) append(body, plateType.getDescription()); diff --git a/assay/src/org/labkey/assay/plate/PlateImpl.java b/assay/src/org/labkey/assay/plate/PlateImpl.java index a3a72ff0b37..5e31ad401e2 100644 --- a/assay/src/org/labkey/assay/plate/PlateImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateImpl.java @@ -18,12 +18,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; 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.PlateSet; +import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.plate.Position; import org.labkey.api.assay.plate.PositionImpl; import org.labkey.api.assay.plate.Well; @@ -50,16 +52,19 @@ public class PlateImpl extends PropertySetImpl implements Plate, Cloneable { private String _name; private Integer _rowId; - private int _rows; - private int _columns; private int _createdBy; private long _created; private int _modifiedBy; private long _modified; private String _dataFileId; - private String _type; + private String _assayType; private boolean _isTemplate; private Integer _plateSetId; + private Integer _plateType; + private String _description; + private String _plateId; + private PlateType _plateTypeObject; + private PlateSet _plateSetObject; private Map> _groups; private List _deletedGroups; @@ -82,20 +87,20 @@ public PlateImpl() _plateNumber = 1; } - public PlateImpl(Container container, String name, String type, int rowCount, int colCount) + public PlateImpl(Container container, String name, String assayType, @NotNull PlateType plateType) { super(container); _name = name; - _type = type; - _rows = rowCount; - _columns = colCount; + _assayType = assayType; _container = container; _dataFileId = GUID.makeGUID(); + _plateType = plateType.getRowId(); + _plateTypeObject = plateType; } public PlateImpl(PlateImpl plate, double[][] wellValues, boolean[][] excluded, int runId, int plateNumber) { - this(plate.getContainer(), plate.getName(), plate.getType(), plate.getRows(), plate.getColumns()); + this(plate.getContainer(), plate.getName(), plate.getAssayType(), plate.getPlateTypeObject()); if (wellValues == null) wellValues = new double[plate.getRows()][plate.getColumns()]; @@ -283,9 +288,12 @@ public Map> getWellGroupMap() } @Override + @JsonIgnore public int getColumns() { - return _columns; + if (_plateTypeObject == null) + return 0; + return _plateTypeObject.getColumns(); } @Override @@ -295,9 +303,12 @@ public String getName() } @Override + @JsonIgnore public int getRows() { - return _rows; + if (_plateTypeObject == null) + return 0; + return _plateTypeObject.getRows(); } @JsonIgnore @@ -317,16 +328,6 @@ public void setRowId(Integer rowId) _rowId = rowId; } - public void setColumns(int columns) - { - _columns = columns; - } - - public void setRows(int rows) - { - _rows = rows; - } - @Override public void setName(String name) { @@ -420,14 +421,14 @@ public int getWellGroupCount(WellGroup.Type type) } @Override - public String getType() + public String getAssayType() { - return _type; + return _assayType; } - public void setType(String type) + public void setAssayType(String type) { - _type = type; + _assayType = type; } @JsonIgnore @@ -543,11 +544,51 @@ public Integer getPlateSet() return _plateSetId; } + public void setPlateType(Integer plateType) + { + _plateType = plateType; + } + + @JsonIgnore // Ignored for client serialization due to full serialization of "plateType" + public Integer getPlateType() + { + return _plateType; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + @Override + @JsonProperty("plateType") + public @NotNull PlateType getPlateTypeObject() + { + if (_plateTypeObject == null) + _plateTypeObject = PlateManager.get().getPlateType(_plateType); + return _plateTypeObject; + } + + public void setPlateTypeObject(PlateType plateTypeObject) + { + _plateTypeObject = plateTypeObject; + } + @Override @JsonIgnore public @Nullable PlateSet getPlateSetObject() { - return PlateManager.get().getPlateSet(getContainer(), getPlateSet()); + return _plateSetObject; + } + + public void setPlateSetObject(PlateSet plateSetObject) + { + _plateSetObject = plateSetObject; } @JsonIgnore @@ -614,4 +655,14 @@ public void setRunCount(Integer runCount) { _runCount = runCount; } + + public String getPlateId() + { + return _plateId; + } + + public void setPlateId(String plateId) + { + _plateId = plateId; + } } diff --git a/assay/src/org/labkey/assay/plate/PlateManager.java b/assay/src/org/labkey/assay/plate/PlateManager.java index 556e115cf18..fda35bad65f 100644 --- a/assay/src/org/labkey/assay/plate/PlateManager.java +++ b/assay/src/org/labkey/assay/plate/PlateManager.java @@ -27,12 +27,13 @@ import org.labkey.api.assay.AssayProvider; import org.labkey.api.assay.AssayService; import org.labkey.api.assay.dilution.DilutionCurve; -import org.labkey.api.assay.plate.AbstractPlateTypeHandler; +import org.labkey.api.assay.plate.AbstractPlateLayoutHandler; import org.labkey.api.assay.plate.Plate; import org.labkey.api.assay.plate.PlateCustomField; +import org.labkey.api.assay.plate.PlateLayoutHandler; import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateSet; -import org.labkey.api.assay.plate.PlateTypeHandler; +import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.plate.PlateUtils; import org.labkey.api.assay.plate.Position; import org.labkey.api.assay.plate.PositionImpl; @@ -85,7 +86,6 @@ import org.labkey.api.query.ValidationException; import org.labkey.api.search.SearchService; import org.labkey.api.security.User; -import org.labkey.api.security.permissions.DeletePermission; import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.Permission; import org.labkey.api.security.permissions.ReadPermission; @@ -98,7 +98,7 @@ import org.labkey.api.view.UnauthorizedException; import org.labkey.api.webdav.WebdavResource; import org.labkey.assay.TsvAssayProvider; -import org.labkey.assay.plate.model.PlateType; +import org.labkey.assay.plate.model.PlateTypeImpl; import org.labkey.assay.plate.model.WellGroupBean; import org.labkey.assay.plate.query.PlateSchema; import org.labkey.assay.plate.query.PlateSetTable; @@ -141,11 +141,11 @@ public class PlateManager implements PlateService private final List _detailsLinkResolvers = new ArrayList<>(); private boolean _lsidHandlersRegistered = false; - private final Map _plateTypeHandlers = new HashMap<>(); + private final Map _plateLayoutHandlers = new HashMap<>(); // name expressions, currently not configurable private static final String PLATE_SET_NAME_EXPRESSION = "PLS-${now:date('yyyyMMdd')}-${RowId}"; - private static final String PLATE_NAME_EXPRESSION = "${${PlateSet/Name}-:withCounter}"; + private static final String PLATE_NAME_EXPRESSION = "${${PlateSet/PlateSetId}-:withCounter}"; public SearchService.SearchCategory PLATE_CATEGORY = new SearchService.SearchCategory("plate", "Plate") { @Override @@ -162,12 +162,13 @@ public static PlateManager get() public PlateManager() { - registerPlateTypeHandler(new AbstractPlateTypeHandler() + registerPlateLayoutHandler(new AbstractPlateLayoutHandler() { @Override - public Plate createTemplate(@Nullable String templateTypeName, Container container, int rowCount, int colCount) + public Plate createTemplate(@Nullable String templateTypeName, Container container, @NotNull PlateType plateType) { - return PlateService.get().createPlateTemplate(container, getAssayType(), rowCount, colCount); + validatePlateType(plateType); + return PlateService.get().createPlateTemplate(container, getAssayType(), plateType); } @Override @@ -177,15 +178,16 @@ public String getAssayType() } @Override - public List getTemplateTypes(Pair size) + @NotNull + public List getLayoutTypes(PlateType plateType) { return new ArrayList<>(); } @Override - public List> getSupportedPlateSizes() + protected List> getSupportedPlateSizes() { - return Collections.singletonList(new Pair<>(8, 12)); + return List.of(new Pair<>(8, 12)); } @Override @@ -221,14 +223,14 @@ public List getWellGroupTypes() @NotNull PlateType plateType, @Nullable String plateName, @Nullable Integer plateSetId, + @Nullable String assayType, @Nullable List> data ) throws Exception { Plate plate = null; try (DbScope.Transaction tx = ensureTransaction()) { - PlateTypeHandler plateTypeHandler = getPlateTypeHandler(plateType.getAssayType()); - Plate plateTemplate = plateTypeHandler.createTemplate(plateType.getType(), container, plateType.getRows(), plateType.getCols()); + Plate plateTemplate = PlateService.get().createPlateTemplate(container, assayType, plateType); plate = createPlate(plateTemplate, null, null); if (plateSetId != null) @@ -408,16 +410,16 @@ public int getRunCountUsingPlate(@NotNull Container c, @NotNull User user, @NotN @Override @NotNull - public Plate createPlate(Container container, String templateType, int rowCount, int colCount) + public Plate createPlate(Container container, String templateType, @NotNull PlateType plateType) { - return new PlateImpl(container, null, templateType, rowCount, colCount); + return new PlateImpl(container, null, templateType, plateType); } @Override @NotNull - public Plate createPlateTemplate(Container container, String templateType, int rowCount, int colCount) + public Plate createPlateTemplate(Container container, String templateType, @NotNull PlateType plateType) { - Plate plate = createPlate(container, templateType, rowCount, colCount); + Plate plate = createPlate(container, templateType, plateType); ((PlateImpl)plate).setTemplate(true); return plate; @@ -547,6 +549,17 @@ public int save(Container container, User user, Plate plate) throws Exception protected void populatePlate(PlateImpl plate) { + // plate type and plate set objects + PlateType plateType = getPlateType(plate.getPlateType()); + if (plateType == null) + throw new IllegalStateException("Unable to get Plate Type with id : " + plate.getPlateType()); + plate.setPlateTypeObject(plateType); + + PlateSet plateSet = getPlateSet(plate.getContainer(), plate.getPlateSet()); + if (plateSet == null) + throw new IllegalStateException("Unable to get Plate Set with id : " + plate.getPlateSet()); + plate.setPlateSetObject(plateSet); + // set plate properties: setProperties(plate.getContainer(), plate); @@ -1048,16 +1061,17 @@ public ActionURL getDetailsURL(Plate plate) return null; } - public List getPlateTypeHandlers() + public List getPlateLayoutHandlers() { - List result = new ArrayList<>(_plateTypeHandlers.values()); - result.sort(Comparator.comparing(PlateTypeHandler::getAssayType, String.CASE_INSENSITIVE_ORDER)); + List result = new ArrayList<>(_plateLayoutHandlers.values()); + result.sort(Comparator.comparing(PlateLayoutHandler::getAssayType, String.CASE_INSENSITIVE_ORDER)); return result; } - public PlateTypeHandler getPlateTypeHandler(String plateTypeName) + @Nullable + public PlateLayoutHandler getPlateLayoutHandler(String plateTypeName) { - return _plateTypeHandlers.get(plateTypeName); + return _plateLayoutHandlers.get(plateTypeName); } private UserSchema getPlateUserSchema(Container container, User user) @@ -1216,7 +1230,7 @@ public Plate copyPlate(Plate source, User user, Container destContainer) { if (plateExists(destContainer, source.getName())) throw new PlateService.NameConflictException(source.getName()); - Plate newPlate = createPlateTemplate(destContainer, source.getType(), source.getRows(), source.getColumns()); + Plate newPlate = createPlateTemplate(destContainer, source.getAssayType(), source.getPlateTypeObject()); newPlate.setName(source.getName()); for (String property : source.getPropertyNames()) newPlate.setProperty(property, source.getProperty(property)); @@ -1234,13 +1248,13 @@ public Plate copyPlate(Plate source, User user, Container destContainer) } @Override - public void registerPlateTypeHandler(PlateTypeHandler handler) + public void registerPlateLayoutHandler(PlateLayoutHandler handler) { - if (_plateTypeHandlers.containsKey(handler.getAssayType())) + if (_plateLayoutHandlers.containsKey(handler.getAssayType())) { throw new IllegalArgumentException(handler.getAssayType()); } - _plateTypeHandlers.put(handler.getAssayType(), handler); + _plateLayoutHandlers.put(handler.getAssayType(), handler); } public void clearCache(Container c, Plate plate) @@ -1259,55 +1273,58 @@ public DilutionCurve getDilutionCurve(List wellGroups, boolean assume return CurveFitFactory.getCurveImpl(wellGroups, assumeDecreasing, percentCalculator, type); } - public List getPlateTypes() + @Override + public List getPlateTypes() + { + return new TableSelector(AssayDbSchema.getInstance().getTableInfoPlateType()).getArrayList(PlateTypeImpl.class); + } + + @Override + @Nullable + public PlateType getPlateType(int rows, int columns) { - List plateTypes = new ArrayList<>(); + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("Rows"), rows); + filter.addCondition(FieldKey.fromParts("Columns"), columns); - for (PlateTypeHandler handler : getPlateTypeHandlers()) + return new TableSelector(AssayDbSchema.getInstance().getTableInfoPlateType(), filter, null).getObject(PlateTypeImpl.class); + } + + public record PlateLayout(String name, PlateType type, String assayType, String description){} + + @NotNull + public List getPlateLayouts() + { + List layouts = new ArrayList<>(); + for (PlateLayoutHandler handler : getPlateLayoutHandlers()) { - for (Pair size : handler.getSupportedPlateSizes()) + for (PlateType type : handler.getSupportedPlateTypes()) { - int rows = size.first; - int cols = size.second; - - int wellCount = rows * cols; - String sizeDescription = wellCount + " well (" + rows + "x" + cols + ") "; + int wellCount = type.getRows() * type.getColumns(); + String sizeDescription = wellCount + " well (" + type.getRows() + "x" + type.getColumns() + ") "; - List types = handler.getTemplateTypes(size); - if (types == null || types.isEmpty()) + List layoutTypes = handler.getLayoutTypes(type); + if (layoutTypes.isEmpty()) { String description = sizeDescription + handler.getAssayType(); - plateTypes.add(new PlateType(handler.getAssayType(), null, description, rows, cols)); + layouts.add(new PlateLayout(null, type, handler.getAssayType(), description)); } else { - for (String type : types) + for (String layoutName : layoutTypes) { - String description = sizeDescription + handler.getAssayType() + " " + type; - plateTypes.add(new PlateType(handler.getAssayType(), type, description, rows, cols)); + String description = sizeDescription + handler.getAssayType() + " " + layoutName; + layouts.add(new PlateLayout(layoutName, type, handler.getAssayType(), description)); } } } } - - return plateTypes; + return layouts; } - public PlateType getPlateType(@NotNull Plate plate) + public PlateType getPlateType(Integer plateTypeId) { - for (PlateType plateType : getPlateTypes()) - { - if ( - plateType.getRows() == plate.getRows() && - Objects.equals(plateType.getCols(), plateType.getCols()) && - Objects.equals(plateType.getType(), plate.getType()) - ) - { - return plateType; - } - } - - return null; + if (plateTypeId == null) return null; + return new TableSelector(AssayDbSchema.getInstance().getTableInfoPlateType()).getObject(plateTypeId, PlateTypeImpl.class); } public @NotNull Map>> getPlateOperationConfirmationData( @@ -1734,7 +1751,9 @@ public List setFields(Container container, User user, Integer return PLATE_NAME_EXPRESSION; } - public PlateSetImpl createPlateSet(Container container, User user, @NotNull PlateSetImpl plateSet, @Nullable List plateTypes) throws Exception + public record CreatePlateSetPlate(String name, Integer plateType) {} + + public PlateSetImpl createPlateSet(Container container, User user, @NotNull PlateSetImpl plateSet, @Nullable List plates) throws Exception { if (!container.hasPermission(user, InsertPermission.class)) throw new UnauthorizedException("Failed to create plate set. Insufficient permissions."); @@ -1742,7 +1761,7 @@ public PlateSetImpl createPlateSet(Container container, User user, @NotNull Plat if (plateSet.getRowId() != null) throw new ValidationException("Failed to create plate set. Cannot create plate set with rowId (" + plateSet.getRowId() + ")."); - if (plateTypes != null && plateTypes.size() > MAX_PLATES) + if (plates != null && plates.size() > MAX_PLATES) throw new ValidationException(String.format("Failed to create plate set. Plate sets can have a maximum of %d plates.", MAX_PLATES)); try (DbScope.Transaction tx = ensureTransaction()) @@ -1757,13 +1776,16 @@ public PlateSetImpl createPlateSet(Container container, User user, @NotNull Plat Integer plateSetId = (Integer) rows.get(0).get("RowId"); - if (plateTypes != null) + if (plates != null) { - for (PlateType plateType : plateTypes) + for (var plate : plates) { + var plateType = getPlateType(plate.plateType); + if (plateType == null) + throw new ValidationException("Failed to create plate set. Plate Type (" + plate.plateType + ") is invalid."); + // TODO: Write a cheaper plate create/save for multiple plates - if (plateType != null) - createAndSavePlate(container, user, plateType, null, plateSetId, null); + createAndSavePlate(container, user, plateType, plate.name, plateSetId, null, null); } } @@ -1852,8 +1874,11 @@ public void createPlateTemplate() throws Exception // INSERT // - PlateTypeHandler handler = PlateManager.get().getPlateTypeHandler(TsvPlateTypeHandler.TYPE); - Plate template = handler.createTemplate("UNUSED", container, 8, 12); + PlateLayoutHandler handler = PlateManager.get().getPlateLayoutHandler(TsvPlateLayoutHandler.TYPE); + PlateType plateType = PlateManager.get().getPlateType(8, 12); + assertNotNull("96 well plate type was not found", plateType); + + Plate template = handler.createTemplate("UNUSED", container, plateType); template.setName("bob"); template.setProperty("friendly", "yes"); assertNull(template.getRowId()); @@ -1878,6 +1903,7 @@ public void createPlateTemplate() throws Exception assertEquals(plateId, savedTemplate.getRowId().intValue()); assertEquals("bob", savedTemplate.getName()); assertEquals("yes", savedTemplate.getProperty("friendly")); assertNotNull(savedTemplate.getLSID()); + assertEquals(plateType.getRowId(), savedTemplate.getPlateTypeObject().getRowId()); List wellGroups = savedTemplate.getWellGroups(); assertEquals(3, wellGroups.size()); @@ -1947,6 +1973,10 @@ public void createPlateTemplate() throws Exception // verify added positions assertEquals(2, updatedControlWellGroups.get(0).getPositions().size()); + // verify plate type information + assertEquals(plateType.getRows().intValue(), updatedTemplate.getRows()); + assertEquals(plateType.getColumns().intValue(), updatedTemplate.getColumns()); + // // DELETE // @@ -1961,10 +1991,11 @@ public void createPlateTemplate() throws Exception public void testCreateAndSavePlate() throws Exception { // Arrange - PlateType plateType = new PlateType(TsvPlateTypeHandler.TYPE, TsvPlateTypeHandler.BLANK_PLATE, "Test plate type", 8, 12); + PlateType plateType = PlateManager.get().getPlateType(8, 12); + assertNotNull("96 well plate type was not found", plateType); // Act - Plate plate = PlateManager.get().createAndSavePlate(container, user, plateType, "testCreateAndSavePlate plate", null, null); + Plate plate = PlateManager.get().createAndSavePlate(container, user, plateType, "testCreateAndSavePlate plate", null, null, null); // Assert assertTrue("Expected plate to have been persisted and provided with a rowId", plate.getRowId() > 0); @@ -1981,7 +2012,9 @@ public void testCreateAndSavePlate() throws Exception public void testCreatePlateTemplates() throws Exception { // Verify plate service assumptions about plate templates - Plate plate = PlateService.get().createPlateTemplate(container, TsvPlateTypeHandler.TYPE, 16, 24); + PlateType plateType = PlateManager.get().getPlateType(16, 24); + assertNotNull("384 well plate type was not found", plateType); + Plate plate = PlateService.get().createPlateTemplate(container, TsvPlateLayoutHandler.TYPE, plateType); plate.setName("my plate template"); int plateId = PlateService.get().save(container, user, plate); @@ -1990,7 +2023,10 @@ public void testCreatePlateTemplates() throws Exception assertTrue("Expected saved plate to have the template field set to true", PlateService.get().getPlate(container, plateId).isTemplate()); // Verify only plate templates are returned - plate = PlateService.get().createPlate(container, TsvPlateTypeHandler.TYPE, 8, 12); + plateType = PlateManager.get().getPlateType(8, 12); + assertNotNull("96 well plate type was not found", plateType); + + plate = PlateService.get().createPlate(container, TsvPlateLayoutHandler.TYPE, plateType); plate.setName("non plate template"); PlateService.get().save(container, user, plate); @@ -2005,7 +2041,10 @@ public void testCreatePlateTemplates() throws Exception @Test public void testCreatePlateMetadata() throws Exception { - Plate plate = PlateService.get().createPlateTemplate(container, TsvPlateTypeHandler.TYPE, 16, 24); + PlateType plateType = PlateManager.get().getPlateType(16, 24); + assertNotNull("384 well plate type was not found", plateType); + + Plate plate = PlateService.get().createPlateTemplate(container, TsvPlateLayoutHandler.TYPE, plateType); plate.setName("new plate with metadata"); int plateId = PlateService.get().save(container, user, plate); @@ -2111,7 +2150,8 @@ else if (row == 1) public void testCreateAndSavePlateWithData() throws Exception { // Arrange - PlateType plateType = new PlateType(TsvPlateTypeHandler.TYPE, TsvPlateTypeHandler.BLANK_PLATE, "Standard 96 well plate", 8, 12); + PlateType plateType = PlateManager.get().getPlateType(8, 12); + assertNotNull("96 well plate type was not found", plateType); // Act List> rows = List.of( @@ -2126,7 +2166,7 @@ public void testCreateAndSavePlateWithData() throws Exception "properties/barcode", "B5678" ) ); - Plate plate = PlateManager.get().createAndSavePlate(container, user, plateType, "hit selection plate", null, rows); + Plate plate = PlateManager.get().createAndSavePlate(container, user, plateType, "hit selection plate", null, null, rows); assertEquals("Expected 2 plate custom fields", 2, plate.getCustomFields().size()); TableInfo wellTable = QueryService.get().getUserSchema(user, container, PlateSchema.SCHEMA_NAME).getTable(WellTable.NAME); diff --git a/assay/src/org/labkey/assay/plate/PlateSetImpl.java b/assay/src/org/labkey/assay/plate/PlateSetImpl.java index a5611f74def..c7fe2f52a14 100644 --- a/assay/src/org/labkey/assay/plate/PlateSetImpl.java +++ b/assay/src/org/labkey/assay/plate/PlateSetImpl.java @@ -26,8 +26,10 @@ public class PlateSetImpl extends Entity implements PlateSet { private Integer _rowId; private String _name; + private String _plateSetId; private boolean _archived; private Container _container; + private String _description; public PlateSetImpl() { @@ -68,6 +70,17 @@ public void setName(String name) _name = name; } + @Override + public String getPlateSetId() + { + return _plateSetId; + } + + public void setPlateSetId(String plateSetId) + { + _plateSetId = plateSetId; + } + @Override public boolean isArchived() { @@ -106,4 +119,14 @@ public Integer getPlateCount() return new SqlSelector(table.getSchema(), sql).getObject(Integer.class); } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } } diff --git a/assay/src/org/labkey/assay/plate/TsvPlateTypeHandler.java b/assay/src/org/labkey/assay/plate/TsvPlateLayoutHandler.java similarity index 62% rename from assay/src/org/labkey/assay/plate/TsvPlateTypeHandler.java rename to assay/src/org/labkey/assay/plate/TsvPlateLayoutHandler.java index 1bfb6dc2a11..f3111b99c95 100644 --- a/assay/src/org/labkey/assay/plate/TsvPlateTypeHandler.java +++ b/assay/src/org/labkey/assay/plate/TsvPlateLayoutHandler.java @@ -1,19 +1,20 @@ package org.labkey.assay.plate; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.labkey.api.assay.plate.AbstractPlateTypeHandler; -import org.labkey.api.assay.plate.PlateService; +import org.labkey.api.assay.plate.AbstractPlateLayoutHandler; import org.labkey.api.assay.plate.Plate; +import org.labkey.api.assay.plate.PlateService; +import org.labkey.api.assay.plate.PlateType; import org.labkey.api.assay.plate.WellGroup; import org.labkey.api.data.Container; import org.labkey.api.util.Pair; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -public class TsvPlateTypeHandler extends AbstractPlateTypeHandler +public class TsvPlateLayoutHandler extends AbstractPlateLayoutHandler { public static final String BLANK_PLATE = "blank"; public static final String TYPE = "Standard"; @@ -25,15 +26,17 @@ public String getAssayType() } @Override - public List getTemplateTypes(Pair size) + @NotNull + public List getLayoutTypes(PlateType plateType) { return Collections.singletonList(BLANK_PLATE); } @Override - public Plate createTemplate(@Nullable String templateTypeName, Container container, int rowCount, int colCount) + public Plate createTemplate(@Nullable String templateTypeName, Container container, @NotNull PlateType plateType) { - Plate template = PlateService.get().createPlateTemplate(container, getAssayType(), rowCount, colCount); + validatePlateType(plateType); + Plate template = PlateService.get().createPlateTemplate(container, getAssayType(), plateType); template.addWellGroup("Positive", WellGroup.Type.CONTROL, Collections.emptyList()); template.addWellGroup("Negative", WellGroup.Type.CONTROL, Collections.emptyList()); @@ -42,17 +45,14 @@ public Plate createTemplate(@Nullable String templateTypeName, Container contain } @Override - public List> getSupportedPlateSizes() + protected List> getSupportedPlateSizes() { - List> sizes = new ArrayList<>(); - sizes.add(new Pair<>(3, 4)); - sizes.add(new Pair<>(4, 6)); - sizes.add(new Pair<>(6, 8)); - sizes.add(new Pair<>(8, 12)); - sizes.add(new Pair<>(16, 24)); - sizes.add(new Pair<>(32, 48)); - - return sizes; + return List.of(new Pair<>(3, 4), + new Pair<>(4, 6), + new Pair<>(6, 8), + new Pair<>(8, 12), + new Pair<>(16, 24), + new Pair<>(32, 48)); } @Override diff --git a/assay/src/org/labkey/assay/plate/model/PlateType.java b/assay/src/org/labkey/assay/plate/model/PlateType.java deleted file mode 100644 index d1ffd7bd02b..00000000000 --- a/assay/src/org/labkey/assay/plate/model/PlateType.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.labkey.assay.plate.model; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class PlateType -{ - private String _assayType; - private Integer _cols; - private String _description; - private Integer _rows; - private String _type; - - public PlateType() - { - } - - public PlateType(String assayType, String type, String description, Integer rows, Integer cols) - { - _assayType = assayType; - _type = type; - _description = description; - _cols = cols; - _rows = rows; - } - - public String getAssayType() - { - return _assayType; - } - - public void setAssayType(String assayType) - { - _assayType = assayType; - } - - public Integer getCols() - { - return _cols; - } - - public void setCols(Integer cols) - { - _cols = cols; - } - - public String getDescription() - { - return _description; - } - - public void setDescription(String description) - { - _description = description; - } - - public Integer getRows() - { - return _rows; - } - - public void setRows(Integer rows) - { - _rows = rows; - } - - public String getType() - { - return _type; - } - - public void setType(String type) - { - _type = type; - } -} diff --git a/assay/src/org/labkey/assay/plate/model/PlateTypeImpl.java b/assay/src/org/labkey/assay/plate/model/PlateTypeImpl.java new file mode 100644 index 00000000000..c9ee38c54fe --- /dev/null +++ b/assay/src/org/labkey/assay/plate/model/PlateTypeImpl.java @@ -0,0 +1,98 @@ +package org.labkey.assay.plate.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.labkey.api.assay.plate.PlateType; + +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PlateTypeImpl implements PlateType +{ + private Integer _rowId; + private Integer _rows; + private Integer _cols; + private String _description; + private boolean _archived; + + public PlateTypeImpl() + { + } + + @Override + public Integer getRowId() + { + return _rowId; + } + + public void setRowId(Integer rowId) + { + _rowId = rowId; + } + + public Integer getCols() + { + return _cols; + } + + @Override + public Integer getColumns() + { + return _cols; + } + + public void setColumns(Integer cols) + { + _cols = cols; + } + + public void setCols(Integer cols) + { + _cols = cols; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public Integer getRows() + { + return _rows; + } + + public void setRows(Integer rows) + { + _rows = rows; + } + + public boolean isArchived() + { + return _archived; + } + + public void setArchived(boolean archived) + { + _archived = archived; + } + + @Override + public int hashCode() + { + return (31 * _rows) + _cols; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + if (!Objects.equals(_rows, ((PlateTypeImpl) obj)._rows)) return false; + return Objects.equals(_cols, ((PlateTypeImpl) obj)._cols); + } +} diff --git a/assay/src/org/labkey/assay/plate/query/NamePlusIdDataIterator.java b/assay/src/org/labkey/assay/plate/query/NamePlusIdDataIterator.java new file mode 100644 index 00000000000..200a7ba37b8 --- /dev/null +++ b/assay/src/org/labkey/assay/plate/query/NamePlusIdDataIterator.java @@ -0,0 +1,88 @@ +package org.labkey.assay.plate.query; + +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.data.Container; +import org.labkey.api.data.NameGenerator; +import org.labkey.api.data.TableInfo; +import org.labkey.api.dataiterator.DataIterator; +import org.labkey.api.dataiterator.DataIteratorContext; +import org.labkey.api.dataiterator.DataIteratorUtil; +import org.labkey.api.dataiterator.MapDataIterator; +import org.labkey.api.dataiterator.WrapperDataIterator; +import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.ValidationException; + +import java.util.Map; + +public class NamePlusIdDataIterator extends WrapperDataIterator +{ + private final DataIteratorContext _context; + private final Integer _nameCol; + private final Integer _idCol; + private final NameGenerator _nameGenerator; + private final NameGenerator.State _state; + private String _generatedName; + + public NamePlusIdDataIterator(DataIterator di, DataIteratorContext context, TableInfo parentTable, Container container, + String nameColumn, String idColumn, String nameExpression) + { + super(DataIteratorUtil.wrapMap(di, false)); + + _context = context; + Map map = DataIteratorUtil.createColumnNameMap(di); + _nameCol = map.get(nameColumn); + _idCol = map.get(idColumn); + + _nameGenerator = new NameGenerator(nameExpression, parentTable, false, container, null, null); + _state = _nameGenerator.createState(false); + } + + MapDataIterator getInput() + { + return (MapDataIterator) _delegate; + } + + @Override + public boolean next() throws BatchValidationException + { + boolean next = super.next(); + if (next) + { + try + { + Map currentRow = new CaseInsensitiveHashMap<>(getInput().getMap()); + // remove the name field so we don't use it + currentRow.put("name", null); + _generatedName = _nameGenerator.generateName(_state, currentRow); + _state.cleanUp(); + } + catch (NameGenerator.NameGenerationException e) + { + _context.getErrors().addRowError(new ValidationException(e.getMessage())); + } + } + return next; + } + + @Override + public Object get(int i) + { + if (i == _nameCol) + { + Object curName = super.get(_nameCol); + if (curName instanceof String) + curName = StringUtils.isEmpty((String) curName) ? null : curName; + + if (curName != null) + return curName; + else + return _generatedName; + } + else if (i == _idCol) + { + return _generatedName; + } + return super.get(i); + } +} diff --git a/assay/src/org/labkey/assay/plate/query/PlateSchema.java b/assay/src/org/labkey/assay/plate/query/PlateSchema.java index b4e3f33f18c..bb58aed7fbe 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateSchema.java +++ b/assay/src/org/labkey/assay/plate/query/PlateSchema.java @@ -44,7 +44,8 @@ public class PlateSchema extends SimpleUserSchema PlateTable.NAME, WellGroupTable.NAME, WellTable.NAME, - PlateSetTable.NAME + PlateSetTable.NAME, + PlateTypeTable.NAME )); public PlateSchema(User user, Container container) @@ -70,6 +71,8 @@ public TableInfo createTable(String name, ContainerFilter cf) return new WellTable(this, cf).init(); if (name.equalsIgnoreCase(PlateSetTable.NAME)) return new PlateSetTable(this, cf).init(); + if (name.equalsIgnoreCase(PlateTypeTable.NAME)) + return new PlateTypeTable(this, cf).init(); return null; } diff --git a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java index c59d2a0bf9a..9e4927ea587 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateSetTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateSetTable.java @@ -2,10 +2,8 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; -import org.labkey.api.assay.plate.PlateService; import org.labkey.api.assay.plate.PlateSet; import org.labkey.api.collections.CaseInsensitiveHashSet; -import org.labkey.api.data.BaseColumnInfo; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerFilter; @@ -19,7 +17,6 @@ import org.labkey.api.dataiterator.DataIteratorContext; import org.labkey.api.dataiterator.DetailedAuditLogDataIterator; import org.labkey.api.dataiterator.LoggingDataIterator; -import org.labkey.api.dataiterator.NameExpressionDataIterator; import org.labkey.api.dataiterator.SimpleTranslator; import org.labkey.api.dataiterator.StandardDataIteratorBuilder; import org.labkey.api.dataiterator.TableInsertDataIteratorBuilder; @@ -54,6 +51,7 @@ public class PlateSetTable extends SimpleUserSchema.SimpleTable { defaultVisibleColumns.add(FieldKey.fromParts("Name")); defaultVisibleColumns.add(FieldKey.fromParts("Container")); + defaultVisibleColumns.add(FieldKey.fromParts("Description")); defaultVisibleColumns.add(FieldKey.fromParts("PlateCount")); defaultVisibleColumns.add(FieldKey.fromParts("Created")); defaultVisibleColumns.add(FieldKey.fromParts("CreatedBy")); @@ -79,12 +77,7 @@ public MutableColumnInfo wrapColumn(ColumnInfo col) { var columnInfo = super.wrapColumn(col); - // the name field is always generated via name expression - if (columnInfo.getName().equalsIgnoreCase("Name")) - { - columnInfo.setUserEditable(false); - } - else if (columnInfo.getName().equalsIgnoreCase("RowId")) + if (columnInfo.getName().equalsIgnoreCase("RowId")) { // this is necessary in order to use rowId as a name expression token columnInfo.setKeyField(true); @@ -142,11 +135,18 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI ColumnInfo nameCol = plateSetTable.getColumn("name"); nameExpressionTranslator.addColumn(nameCol, (Supplier) () -> null); } - nameExpressionTranslator.addColumn(new BaseColumnInfo("nameExpression", JdbcType.VARCHAR), - (Supplier) () -> PlateService.get().getPlateSetNameExpression()); + if (!nameMap.containsKey("plateSetId")) + { + ColumnInfo nameCol = plateSetTable.getColumn("plateSetId"); + nameExpressionTranslator.addColumn(nameCol, (Supplier) () -> null); + } DataIterator builtInColumnsTranslator = SimpleTranslator.wrapBuiltInColumns(nameExpressionTranslator, context, container, user, plateSetTable); - DataIterator di = LoggingDataIterator.wrap(new NameExpressionDataIterator(builtInColumnsTranslator, context, plateSetTable, container, null, null, null)); + DataIterator di = LoggingDataIterator.wrap(new NamePlusIdDataIterator(builtInColumnsTranslator, context, plateSetTable, + container, + "name", + "plateSetId", + PlateManager.get().getPlateSetNameExpression())); DataIteratorBuilder insertBuilder = LoggingDataIterator.wrap(StandardDataIteratorBuilder.forInsert(getDbTable(), di, container, user, context)); DataIteratorBuilder dib = new TableInsertDataIteratorBuilder(insertBuilder, plateSetTable, container) .setKeyColumns(new CaseInsensitiveHashSet("RowId")); diff --git a/assay/src/org/labkey/assay/plate/query/PlateTable.java b/assay/src/org/labkey/assay/plate/query/PlateTable.java index 76e48a04dd8..b686ad82ef9 100644 --- a/assay/src/org/labkey/assay/plate/query/PlateTable.java +++ b/assay/src/org/labkey/assay/plate/query/PlateTable.java @@ -20,9 +20,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.assay.plate.Plate; -import org.labkey.api.assay.plate.PlateService; import org.labkey.api.collections.CaseInsensitiveHashSet; -import org.labkey.api.data.BaseColumnInfo; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.CompareType; import org.labkey.api.data.Container; @@ -30,6 +28,7 @@ import org.labkey.api.data.DbScope; import org.labkey.api.data.JdbcType; import org.labkey.api.data.MutableColumnInfo; +import org.labkey.api.data.SQLFragment; import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.TableInfo; import org.labkey.api.data.TableSelector; @@ -39,7 +38,6 @@ import org.labkey.api.dataiterator.DataIteratorContext; import org.labkey.api.dataiterator.DetailedAuditLogDataIterator; import org.labkey.api.dataiterator.LoggingDataIterator; -import org.labkey.api.dataiterator.NameExpressionDataIterator; import org.labkey.api.dataiterator.SimpleTranslator; import org.labkey.api.dataiterator.StandardDataIteratorBuilder; import org.labkey.api.dataiterator.TableInsertDataIteratorBuilder; @@ -52,6 +50,7 @@ import org.labkey.api.query.AliasedColumn; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.DefaultQueryUpdateService; +import org.labkey.api.query.ExprColumn; import org.labkey.api.query.FieldKey; import org.labkey.api.query.InvalidKeyException; import org.labkey.api.query.PropertyForeignKey; @@ -73,6 +72,8 @@ import java.util.TreeMap; import java.util.function.Supplier; +import static org.labkey.api.query.ExprColumn.STR_TABLE_ALIAS; + public class PlateTable extends SimpleUserSchema.SimpleTable { public static final String NAME = "Plate"; @@ -81,9 +82,11 @@ public class PlateTable extends SimpleUserSchema.SimpleTable static { defaultVisibleColumns.add(FieldKey.fromParts("Name")); - defaultVisibleColumns.add(FieldKey.fromParts("Rows")); - defaultVisibleColumns.add(FieldKey.fromParts("Columns")); - defaultVisibleColumns.add(FieldKey.fromParts("Type")); + defaultVisibleColumns.add(FieldKey.fromParts("Description")); + defaultVisibleColumns.add(FieldKey.fromParts("PlateType")); + defaultVisibleColumns.add(FieldKey.fromParts("PlateSet")); + defaultVisibleColumns.add(FieldKey.fromParts("AssayType")); + defaultVisibleColumns.add(FieldKey.fromParts("WellsFilled")); defaultVisibleColumns.add(FieldKey.fromParts("Created")); defaultVisibleColumns.add(FieldKey.fromParts("CreatedBy")); defaultVisibleColumns.add(FieldKey.fromParts("Modified")); @@ -101,6 +104,7 @@ public void addColumns() { super.addColumns(); addColumn(createPropertiesColumn()); + addWellsFilledColumn(); } @Override @@ -135,6 +139,17 @@ private MutableColumnInfo createPropertiesColumn() return col; } + private void addWellsFilledColumn() + { + SQLFragment sql = new SQLFragment("(SELECT COUNT(*) AS wellsFilled FROM ") + .append(AssayDbSchema.getInstance().getTableInfoWell(), "") + .append(" WHERE PlateId = " + STR_TABLE_ALIAS + ".RowId") + .append(" AND sampleId IS NOT NULL)"); + ExprColumn countCol = new ExprColumn(this, "WellsFilled", sql, JdbcType.INTEGER); + countCol.setDescription("The number of wells that have samples for this plate."); + addColumn(countCol); + } + @Override public QueryUpdateService getUpdateService() { @@ -198,10 +213,19 @@ public DataIteratorBuilder createImportDIB(User user, Container container, DataI nameExpressionTranslator.addColumn(nameCol, (Supplier) () -> null); } - nameExpressionTranslator.addColumn(new BaseColumnInfo("nameExpression", JdbcType.VARCHAR), - (Supplier) () -> PlateService.get().getPlateNameExpression()); + if (!nameMap.containsKey("plateId")) + { + ColumnInfo nameCol = plateTable.getColumn("plateId"); + nameExpressionTranslator.addColumn(nameCol, (Supplier) () -> null); + } + DataIterator builtInColumnsTranslator = SimpleTranslator.wrapBuiltInColumns(nameExpressionTranslator, context, container, user, plateTable); - DataIterator di = LoggingDataIterator.wrap(new NameExpressionDataIterator(builtInColumnsTranslator, context, plateTable, container, null, null, null)); + + DataIterator di = LoggingDataIterator.wrap(new NamePlusIdDataIterator(builtInColumnsTranslator, context, plateTable, + container, + "name", + "plateId", + PlateManager.get().getPlateNameExpression())); ValidatorIterator vi = new ValidatorIterator(di, context, container, user); vi.addValidator(nameMap.get("name"), new UniquePlateNameValidator(container)); @@ -234,20 +258,20 @@ protected Map updateRow(User user, Container container, Map 0) throw new QueryUpdateServiceException(String.format("%s is used by %d runs and cannot be updated", plate.isTemplate() ? "Plate template" : "Plate", runsInUse)); - // disallow plate size changes - if ((row.containsKey("rows") && ObjectUtils.notEqual(oldRow.get("rows"), row.get("rows"))) || - (row.containsKey("columns") && ObjectUtils.notEqual(oldRow.get("columns"), row.get("columns")))) - { - throw new QueryUpdateServiceException("Changing the plate size (rows or columns) is not allowed."); - } + // disallow plate type changes + if (row.containsKey("plateType") && ObjectUtils.notEqual(oldRow.get("plateType"), row.get("plateType"))) + throw new QueryUpdateServiceException("Changing the plate type is not allowed."); // if the name is changing, check for duplicates - String oldName = (String) oldRow.get("Name"); - String newName = (String) row.get("Name"); - if (!newName.equals(oldName)) + if (row.containsKey("Name")) { - if (PlateManager.get().plateExists(container, newName)) - throw new QueryUpdateServiceException("Plate with name : " + newName + " already exists in the folder."); + String oldName = (String) oldRow.get("Name"); + String newName = (String) row.get("Name"); + if (newName != null && !newName.equals(oldName)) + { + if (PlateManager.get().plateExists(container, newName)) + throw new QueryUpdateServiceException("Plate with name : " + newName + " already exists in the folder."); + } } Map newRow = super.updateRow(user, container, row, oldRow); diff --git a/assay/src/org/labkey/assay/plate/query/PlateTypeTable.java b/assay/src/org/labkey/assay/plate/query/PlateTypeTable.java new file mode 100644 index 00000000000..b1dc0ff2dad --- /dev/null +++ b/assay/src/org/labkey/assay/plate/query/PlateTypeTable.java @@ -0,0 +1,37 @@ +package org.labkey.assay.plate.query; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.ContainerFilter; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.SimpleUserSchema; +import org.labkey.api.query.UserSchema; +import org.labkey.assay.query.AssayDbSchema; + +import java.util.ArrayList; +import java.util.List; + +public class PlateTypeTable extends SimpleUserSchema.SimpleTable +{ + private static final List defaultVisibleColumns = new ArrayList<>(); + + static + { + defaultVisibleColumns.add(FieldKey.fromParts("Description")); + defaultVisibleColumns.add(FieldKey.fromParts("Rows")); + defaultVisibleColumns.add(FieldKey.fromParts("Columns")); + } + + public static final String NAME = "PlateType"; + + public PlateTypeTable(PlateSchema schema, @Nullable ContainerFilter cf) + { + super(schema, AssayDbSchema.getInstance().getTableInfoPlateType(), cf); + setTitleColumn("Description"); + } + + @Override + public List getDefaultVisibleColumns() + { + return defaultVisibleColumns; + } +} diff --git a/assay/src/org/labkey/assay/plate/view/plateTemplateList.jsp b/assay/src/org/labkey/assay/plate/view/plateTemplateList.jsp index 9d09936f2ba..b7655a412ad 100644 --- a/assay/src/org/labkey/assay/plate/view/plateTemplateList.jsp +++ b/assay/src/org/labkey/assay/plate/view/plateTemplateList.jsp @@ -15,8 +15,8 @@ * limitations under the License. */ %> -<%@ page import="org.labkey.api.assay.plate.PlateService" %> <%@ page import="org.labkey.api.assay.plate.Plate" %> +<%@ page import="org.labkey.api.assay.plate.PlateService" %> <%@ page import="org.labkey.api.assay.security.DesignAssayPermission" %> <%@ page import="org.labkey.api.data.Container" %> <%@ page import="org.labkey.api.security.permissions.DeletePermission" %> @@ -35,8 +35,6 @@ <%@ page import="java.util.HashMap" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Map" %> -<%@ page import="org.labkey.assay.plate.model.PlateType" %> -<%@ page import="org.labkey.api.assay.plate.Plate" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> @@ -133,7 +131,7 @@ %> <%= h(template.getName()) %> - <%= h(template.getType()) %> + <%= h(template.getAssayType()) %> <%= h(runCount) %> <% @@ -205,18 +203,18 @@ if (isAssayDesigner || c.hasPermission(getUser(), InsertPermission.class)) { List