diff --git a/api/src/org/labkey/api/exp/api/ExperimentService.java b/api/src/org/labkey/api/exp/api/ExperimentService.java index 47c7ce6313e..4a4245f65dd 100644 --- a/api/src/org/labkey/api/exp/api/ExperimentService.java +++ b/api/src/org/labkey/api/exp/api/ExperimentService.java @@ -100,6 +100,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -797,6 +798,16 @@ static void validateParentAlias(Map aliasMap, Set reserv List getAllExpDataByURL(File f, @Nullable Container c); List getAllExpDataByURL(Path p, @Nullable Container c); + /** The per-sample facts an operation confirmation or a status pre-check needs, read straight off exp.material. */ + record SampleOperationRow(long rowId, Container container, Long sampleState, String cpasType) {} + + /** + * Streams the facts an operation confirmation needs about each sample. A confirmation can cover tens of thousands + * of samples at once, so nothing is collected here and nothing wider than these four columns is read; callers fold + * the rows as they arrive. + */ + void forEachSampleOperationRow(Collection rowIds, Consumer block); + TableInfo getTinfoMaterial(); TableInfo getTinfoMaterialAncestors(); diff --git a/api/src/org/labkey/api/exp/api/SampleTypeService.java b/api/src/org/labkey/api/exp/api/SampleTypeService.java index 86ed77fd53c..b51779982bb 100644 --- a/api/src/org/labkey/api/exp/api/SampleTypeService.java +++ b/api/src/org/labkey/api/exp/api/SampleTypeService.java @@ -267,6 +267,9 @@ default Map incrementSampleCounts(@Nullable Date counterDate) String getOperationNotPermittedMessage(Collection samples, SampleOperations operation); + /** @param totalCount how many samples are blocked, which exceeds the sample list when the caller caps what it collects */ + String getOperationNotPermittedMessage(Collection samples, int totalCount, SampleOperations operation); + int recomputeSampleTypeRollup(ExpSampleType sampleType, Container container) throws SQLException; int recomputeSamplesRollup(Collection sampleIds, String sampleTypeMetricUnit, Container container) throws IllegalStateException, SQLException; diff --git a/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java b/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java index 8a8941fa87e..b5d722589b0 100644 --- a/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExpMaterialImpl.java @@ -133,14 +133,7 @@ public ActionURL detailsURL(Container container, boolean checkForOverride) @Override public @Nullable ExpSampleTypeImpl getSampleType() { - String type = _object.getCpasType(); - if (!ExpMaterial.DEFAULT_CPAS_TYPE.equals(type) && !"Sample".equals(type)) - { - // try current container first (uses cache) - return SampleTypeServiceImpl.get().getSampleTypeByType(type, getContainer()); - } - - return null; + return ExperimentServiceImpl.get().getSampleTypeForCpasType(_object.getCpasType(), getContainer()); } @Override diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java index 7b402e0ec28..0baee0d5919 100644 --- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java @@ -300,6 +300,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -1052,6 +1053,26 @@ public List getExpMaterials(Collection rowIds) return getExpMaterials(new SimpleFilter().addInClause(FieldKey.fromParts(ExpMaterialTable.Column.RowId.name()), rowIds)); } + @Override + public void forEachSampleOperationRow(Collection rowIds, Consumer block) + { + if (rowIds.isEmpty()) + return; + + SimpleFilter filter = new SimpleFilter().addInClause(FieldKey.fromParts(ExpMaterialTable.Column.RowId.name()), rowIds); + Set columns = Set.of("RowId", "Container", "SampleState", "CpasType"); + new TableSelector(getTinfoMaterial(), columns, filter, null).forEach(Material.class, + m -> block.accept(new SampleOperationRow(m.getRowId(), m.getContainer(), m.getSampleState(), m.getCpasType()))); + } + + public @Nullable ExpSampleTypeImpl getSampleTypeForCpasType(@Nullable String cpasType, Container container) + { + if (cpasType == null || ExpMaterial.DEFAULT_CPAS_TYPE.equals(cpasType) || "Sample".equals(cpasType)) + return null; + + return SampleTypeServiceImpl.get().getSampleTypeByType(cpasType, container); + } + @Override @NotNull public List getExpMaterialsByLsid(Collection lsids) @@ -4808,7 +4829,7 @@ private void deleteProtocolInputs(@NotNull Protocol protocol, Collection>> partitionRequestedOperationObjects(User user, Collection requestIds, Collection notAllowedIds, List allData) { - List allowedIds = new LongArrayList(requestIds); + Set allowedIds = new HashSet<>(requestIds); allowedIds.removeAll(notAllowedIds); List> allowedRows = new ArrayList<>(); List> notAllowedRows = new ArrayList<>(); @@ -4833,13 +4854,12 @@ public static Map>> partitionRequestedOpe * For set of rows selected for deletion, find get all linked-to-study datasets that will be affected by the delete * and warn user */ - public static ArrayList> includeLinkedToStudyText(List allMaterials, Set deletable, User user, Container container) + public static ArrayList> includeLinkedToStudyText(@Nullable ExpSampleType sampleType, Set deletable, User user, Container container) { ArrayList> associatedDatasets = new ArrayList<>(); StudyPublishService studyPublishService = StudyPublishService.get(); - if (studyPublishService != null && !allMaterials.isEmpty()) + if (studyPublishService != null && sampleType != null) { - ExpSampleType sampleType = allMaterials.getFirst().getSampleType(); UserSchema userSchema = QueryService.get().getUserSchema(user, container, SamplesSchema.SCHEMA_NAME); TableInfo tableInfo = userSchema.getTable(sampleType.getName()); if (tableInfo == null) diff --git a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java index a2fad787551..9ca532f6790 100644 --- a/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java +++ b/experiment/src/org/labkey/experiment/api/SampleTypeServiceImpl.java @@ -1479,18 +1479,25 @@ public Collection getSamplesNotPermitted(Collection samples, SampleOperations operation) + { + return getOperationNotPermittedMessage(samples, samples.size(), operation); + } + + @Override + public String getOperationNotPermittedMessage(Collection samples, int totalCount, SampleOperations operation) { String message; - if (samples.size() == 1) + if (totalCount == 1 && samples.size() == 1) { ExpMaterial sample = samples.iterator().next(); message = "Sample " + sample.getName() + " has status " + sample.getStateLabel() + ", which prevents"; } else { - message = samples.size() + " samples ("; - message += samples.stream().limit(10).map(ExpMaterial::getNameAndStatus).collect(Collectors.joining(", ")); - if (samples.size() > 10) + int named = Math.min(samples.size(), 10); + message = totalCount + " samples ("; + message += samples.stream().limit(named).map(ExpMaterial::getNameAndStatus).collect(Collectors.joining(", ")); + if (totalCount > named) message += " ..."; message += ") have statuses that prevent"; } diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java index b60e0eb86ab..01ff03106eb 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java +++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java @@ -340,6 +340,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; @@ -3786,30 +3787,52 @@ public Object execute(MaterialOperationConfirmationForm form, BindException erro { Set requestIds = form.getIds(false); ExperimentServiceImpl service = ExperimentServiceImpl.get(); - List allMaterials = service.getExpMaterials(requestIds); + User user = getUser(); + SampleTypeService.SampleOperations operation = form.getSampleOperation(); + Class permClass = operation.getPermissionClass(); + boolean statusSupported = SampleStatusService.get().supportsSampleStatus(); Set notAllowedIds = new HashSet<>(); // We prevent deletion if a sample is used as a parent, has assay data, is used in a job, etc. - if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) + if (operation == SampleTypeService.SampleOperations.Delete) service.getObjectReferencers().forEach(referencer -> notAllowedIds.addAll(referencer.getItemsWithReferences(requestIds, "samples"))); - if (SampleStatusService.get().supportsSampleStatus()) - notAllowedIds.addAll(service.findIdsNotPermittedForOperation(allMaterials, form.getSampleOperation())); - - Map>> response = ExperimentServiceImpl.partitionRequestedOperationObjects(getUser(), requestIds, notAllowedIds, allMaterials); - Collection containers = new HashSet<>(); Collection notPermittedIds = new ArrayList<>(); - Class permClass = form.getSampleOperation().getPermissionClass(); - for (ExpMaterial material : allMaterials) - { - Container c = material.getContainer(); - if (c.hasPermission(getUser(), ReadPermission.class)) + List> rows = new ArrayList<>(); + AtomicReference sampleType = new AtomicReference<>(); + + service.forEachSampleOperationRow(requestIds, row -> { + Container c = row.container(); + boolean canRead = c.hasPermission(user, ReadPermission.class); + if (canRead) containers.add(c); - if (permClass != null && !c.hasPermission(getUser(), permClass)) - notPermittedIds.add(material.getRowId()); - } + if (permClass != null && !c.hasPermission(user, permClass)) + notPermittedIds.add(row.rowId()); + if (statusSupported && !SampleStatusService.get().isOperationPermitted(c, row.sampleState(), operation)) + notAllowedIds.add(row.rowId()); + if (rows.isEmpty()) + sampleType.set(service.getSampleTypeForCpasType(row.cpasType(), c)); + + Map rowMap = new HashMap<>(); + rowMap.put("RowId", row.rowId()); + if (canRead) + rowMap.put("ContainerPath", c.getPath()); + rows.add(rowMap); + }); + + // the status check above feeds notAllowedIds, so the split waits until every row has been seen + Set allowedIds = new HashSet<>(requestIds); + allowedIds.removeAll(notAllowedIds); + List> allowedRows = new ArrayList<>(); + List> notAllowedRows = new ArrayList<>(); + for (Map rowMap : rows) + (allowedIds.contains((Long) rowMap.get("RowId")) ? allowedRows : notAllowedRows).add(rowMap); + + Map>> response = new HashMap<>(); + response.put("allowed", allowedRows); + response.put("notAllowed", notAllowedRows); NameExpressionOptionService svc = NameExpressionOptionService.get(); @@ -3824,7 +3847,7 @@ public Object execute(MaterialOperationConfirmationForm form, BindException erro if (form.getSampleOperation() == SampleTypeService.SampleOperations.Delete) // String 'associatedDatasets' must be synced to its handling in confirmDelete.js, confirmDelete() - response.put("associatedDatasets", ExperimentServiceImpl.includeLinkedToStudyText(allMaterials, requestIds, getUser(), getContainer())); + response.put("associatedDatasets", ExperimentServiceImpl.includeLinkedToStudyText(sampleType.get(), requestIds, getUser(), getContainer())); return success(response); }