Skip to content

Commit af10c5f

Browse files
wow-such-codeSteffengreinerKochTobi
authored
check for datasets, only delete samples if none attached (#436)
* Add basic draft for batch edit and deletion * Remove batchdetailscomponent from sampledetailscomponent * Add deletion notification and base batch adaption on events * Remove unnecessary qualified imports from JD * check for datasets, only delete samples if none attached * WIP Sample deletion in openBis not working and transactional not working as well * add method to update a list of samples * Ensure that batch deletion is only propagated if th * WIP Introduce Batch Editing backend * workaround for safely deleting samples * added todo * add error notification for data attached case * WIP how to convert * remove unnecessary try catch * fix nullpointer when mapping unknown analyte * simplify sample id csv-building for errors * extract sample update creation method * Overhaul of sample and batch registration update and editing # Co-authored-by: KochTobi <[email protected]> * Commented out faulty openbis update method * Comment on why openbis update sample fails * Only call services in edit batch when samples are provided * Remove unused sampledeleted Domain Event and propagation * Remove outdated ToDo * Use correct response code in BatchDomainService * Only open dialog if experimental groups are provided * Rename batch creation event * Resolve wrong merge in dialog Co-authored-by: KochTobi <[email protected]> * Add JD to SampleUpdateRequest Co-authored-by: KochTobi <[email protected]> * Remove ToDo Co-authored-by: KochTobi <[email protected]> --------- Co-authored-by: Steffengreiner <[email protected]> Co-authored-by: Steffengreiner <[email protected]> Co-authored-by: KochTobi <[email protected]>
1 parent 74ff418 commit af10c5f

File tree

26 files changed

+906
-325
lines changed

26 files changed

+906
-325
lines changed

application-commons/src/main/java/life/qbic/application/commons/ApplicationException.java

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public enum ErrorCode {
144144
NO_SPECIES_DEFINED,
145145
NO_SPECIMEN_DEFINED,
146146
NO_ANALYTE_DEFINED,
147+
DATA_ATTACHED_TO_SAMPLES
147148
;
148149

149150
@Override

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/batch/BatchJpaRepository.java

+11
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,15 @@ public Result<Collection<Batch>, ResponseCode> findBatchesByExperimentId(
7979
}
8080
return Result.fromValue(batches);
8181
}
82+
83+
@Override
84+
public Result<BatchId, ResponseCode> deleteById(BatchId batchId) {
85+
try {
86+
qbicBatchRepo.deleteById(batchId);
87+
} catch (Exception e) {
88+
log.error(e.getMessage(), e);
89+
return Result.fromError(ResponseCode.BATCH_DELETION_FAILED);
90+
}
91+
return Result.fromValue(batchId);
92+
}
8293
}

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/QbicSampleDataRepo.java

+7-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package life.qbic.projectmanagement.infrastructure.sample;
22

3+
import java.util.Collection;
34
import java.util.List;
45
import life.qbic.projectmanagement.domain.model.project.Project;
6+
import life.qbic.projectmanagement.domain.model.project.ProjectCode;
57
import life.qbic.projectmanagement.domain.model.sample.Sample;
68
import life.qbic.projectmanagement.domain.model.sample.SampleCode;
79

@@ -26,20 +28,13 @@ public interface QbicSampleDataRepo {
2628
void addSamplesToProject(Project project, List<Sample> samples);
2729

2830
/**
29-
* Deletes a sample with the provided code from persistence.
31+
* Removes the provided samples from persistence
3032
*
31-
* @param sampleCode the {@link SampleCode} of the sample to delete
32-
* @since 1.0.0
33+
* @param projectCode the {@link ProjectCode} of the project these samples belong to
34+
* @param samples the {@link Sample} to be removed from the data repository
3335
*/
34-
void delete(SampleCode sampleCode);
36+
void deleteAll(ProjectCode projectCode, Collection<SampleCode> samples);
3537

36-
/**
37-
* Searches for samples that contain the provided sample code
38-
*
39-
* @param sampleCode the {@link SampleCode} to search for in the data repository
40-
* @return true, if a sample with that code already exists in the system, false if not
41-
* @since 1.0.0
42-
*/
43-
boolean sampleExists(SampleCode sampleCode);
38+
void updateAll(Collection<Sample> samples);
4439

4540
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package life.qbic.projectmanagement.infrastructure.sample;
22

33
import java.util.Collection;
4+
import java.util.List;
5+
import life.qbic.projectmanagement.domain.model.batch.BatchId;
46
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
57
import life.qbic.projectmanagement.domain.model.sample.Sample;
68
import life.qbic.projectmanagement.domain.model.sample.SampleId;
79
import org.springframework.data.jpa.repository.JpaRepository;
10+
811
public interface QbicSampleRepository extends JpaRepository<Sample, SampleId> {
912

10-
Collection<Sample> findAllByExperimentId(ExperimentId experimentId);
13+
Collection<Sample> findAllByExperimentId(ExperimentId experimentId);
14+
15+
List<Sample> findAllByAssignedBatch(BatchId batchId);
1116
}

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/SampleRepositoryImpl.java

+50-5
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,28 @@
22

33
import static life.qbic.logging.service.LoggerFactory.logger;
44

5-
import java.util.ArrayList;
65
import java.util.Collection;
6+
import java.util.List;
77
import java.util.Objects;
88
import java.util.stream.Collectors;
9+
import life.qbic.application.commons.ApplicationException;
10+
import life.qbic.application.commons.ApplicationException.ErrorCode;
11+
import life.qbic.application.commons.ApplicationException.ErrorParameters;
912
import life.qbic.application.commons.Result;
1013
import life.qbic.logging.api.Logger;
1114
import life.qbic.projectmanagement.application.sample.SampleInformationService;
15+
import life.qbic.projectmanagement.domain.model.batch.BatchId;
1216
import life.qbic.projectmanagement.domain.model.experiment.ExperimentId;
1317
import life.qbic.projectmanagement.domain.model.project.Project;
1418
import life.qbic.projectmanagement.domain.model.sample.Sample;
19+
import life.qbic.projectmanagement.domain.model.sample.SampleCode;
1520
import life.qbic.projectmanagement.domain.model.sample.SampleId;
1621
import life.qbic.projectmanagement.domain.repository.SampleRepository;
1722
import life.qbic.projectmanagement.domain.service.SampleDomainService.ResponseCode;
23+
import life.qbic.projectmanagement.infrastructure.sample.openbis.OpenbisConnector.SampleNotDeletedException;
1824
import org.springframework.beans.factory.annotation.Autowired;
1925
import org.springframework.stereotype.Service;
26+
import org.springframework.transaction.annotation.Transactional;
2027

2128

2229
/**
@@ -51,10 +58,8 @@ public SampleRepositoryImpl(QbicSampleRepository qbicSampleRepository,
5158
@Override
5259
public Result<Collection<Sample>, ResponseCode> addAll(Project project,
5360
Collection<Sample> samples) {
54-
Collection<SampleId> sampleIds = new ArrayList<>();
55-
samples.forEach(sample -> sampleIds.add(sample.sampleId()));
56-
String commaSeperatedSampleIds = sampleIds.stream().map(Object::toString).collect(
57-
Collectors.joining(", "));
61+
String commaSeperatedSampleIds = buildCommaSeparatedSampleIds(
62+
samples.stream().map(Sample::sampleId).toList());
5863
try {
5964
this.qbicSampleRepository.saveAll(samples);
6065
} catch (Exception e) {
@@ -72,6 +77,26 @@ public Result<Collection<Sample>, ResponseCode> addAll(Project project,
7277
return Result.fromValue(samples);
7378
}
7479

80+
private String buildCommaSeparatedSampleIds(Collection<SampleId> sampleIds) {
81+
return sampleIds.stream().map(SampleId::toString).collect(Collectors.joining(", "));
82+
}
83+
84+
@Transactional
85+
@Override
86+
public void deleteAll(Project project,
87+
Collection<SampleId> samples) {
88+
List<SampleCode> sampleCodes = qbicSampleRepository.findAllById(samples)
89+
.stream().map(Sample::sampleCode).toList();
90+
this.qbicSampleRepository.deleteAllById(samples);
91+
try {
92+
sampleDataRepo.deleteAll(project.getProjectCode(), sampleCodes);
93+
} catch (SampleNotDeletedException sampleNotDeletedException) {
94+
throw new ApplicationException("Could not delete " + buildCommaSeparatedSampleIds(samples),
95+
sampleNotDeletedException,
96+
ErrorCode.DATA_ATTACHED_TO_SAMPLES, ErrorParameters.empty());
97+
}
98+
}
99+
75100
@Override
76101
public Result<Collection<Sample>, SampleInformationService.ResponseCode> findSamplesByExperimentId(
77102
ExperimentId experimentId) {
@@ -87,4 +112,24 @@ public Result<Collection<Sample>, SampleInformationService.ResponseCode> findSam
87112
return Result.fromValue(samples);
88113
}
89114

115+
@Override
116+
public List<Sample> findSamplesByBatchId(
117+
BatchId batchId) {
118+
Objects.requireNonNull(batchId, "batchId must not be null");
119+
return qbicSampleRepository.findAllByAssignedBatch(batchId);
120+
}
121+
122+
@Transactional
123+
@Override
124+
public void updateAll(Project project,
125+
Collection<Sample> updatedSamples) {
126+
qbicSampleRepository.saveAll(updatedSamples);
127+
sampleDataRepo.updateAll(updatedSamples);
128+
}
129+
130+
@Override
131+
public List<Sample> findSamplesBySampleId(List<SampleId> sampleId) {
132+
return qbicSampleRepository.findAllById(sampleId);
133+
}
134+
90135
}

project-management-infrastructure/src/main/java/life/qbic/projectmanagement/infrastructure/sample/openbis/OpenbisConnector.java

+110-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package life.qbic.projectmanagement.infrastructure.sample.openbis;
22

33
import static life.qbic.logging.service.LoggerFactory.logger;
4+
import static life.qbic.openbis.openbisclient.helper.OpenBisClientHelper.fetchSamplesCompletely;
45

56
import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
67
import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperation;
@@ -26,19 +27,22 @@
2627
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.CreateSamplesOperation;
2728
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.SampleCreation;
2829
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.delete.SampleDeletionOptions;
29-
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.fetchoptions.SampleFetchOptions;
3030
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
31-
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.search.SampleSearchCriteria;
31+
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.update.SampleUpdate;
32+
import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.update.UpdateSamplesOperation;
3233
import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId;
3334
import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.fetchoptions.VocabularyTermFetchOptions;
3435
import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.search.VocabularyTermSearchCriteria;
3536
import java.util.ArrayList;
37+
import java.util.Collection;
3638
import java.util.Collections;
3739
import java.util.HashMap;
40+
import java.util.HashSet;
3841
import java.util.List;
3942
import java.util.Map;
4043
import java.util.Objects;
4144
import java.util.Optional;
45+
import java.util.Set;
4246
import java.util.regex.Matcher;
4347
import java.util.regex.Pattern;
4448
import life.qbic.logging.api.Logger;
@@ -129,13 +133,13 @@ private List<VocabularyTerm> getVocabularyTermsForCode(VocabularyCode vocabulary
129133
.toList();
130134
}
131135

132-
private List<Experiment> searchExperimentsByCode(String projectCode) {
136+
private List<Experiment> searchExperimentsByProjectCode(String projectCode,
137+
ExperimentFetchOptions fetchOptions) {
133138
ExperimentSearchCriteria criteria = new ExperimentSearchCriteria();
134139
criteria.withProject().withCode().thatEquals(projectCode);
135140

136-
ExperimentFetchOptions options = new ExperimentFetchOptions();
137141
SearchResult<Experiment> searchResult =
138-
openBisClient.getV3().searchExperiments(openBisClient.getSessionToken(), criteria, options);
142+
openBisClient.getV3().searchExperiments(openBisClient.getSessionToken(), criteria, fetchOptions);
139143

140144
return searchResult.getObjects();
141145
}
@@ -152,17 +156,6 @@ private List<ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project> searchPro
152156
return searchResult.getObjects();
153157
}
154158

155-
private List<Sample> searchSamplesByCode(String code) {
156-
SampleSearchCriteria criteria = new SampleSearchCriteria();
157-
criteria.withCode().thatEquals(code);
158-
159-
SampleFetchOptions options = new SampleFetchOptions();
160-
SearchResult<Sample> searchResult =
161-
openBisClient.getV3().searchSamples(openBisClient.getSessionToken(), criteria, options);
162-
163-
return searchResult.getObjects();
164-
}
165-
166159
@Override
167160
public List<Species> retrieveSpecies() {
168161
return getVocabularyTermsForCode(VocabularyCode.SPECIES).stream()
@@ -235,12 +228,12 @@ public void addSamplesToProject(Project project,
235228

236229
private Optional<String> retrieveOpenBisAnalyteCode(String analyteLabel) {
237230
return getVocabularyTermsForCode(VocabularyCode.ANALYTE).stream()
238-
.filter(vocabularyTerm -> vocabularyTerm.label.equals(analyteLabel))
231+
.filter(vocabularyTerm -> analyteLabel.equals(vocabularyTerm.label))
239232
.map(vocabularyTerm -> vocabularyTerm.code).findFirst();
240233
}
241234

242235
private String findFreeExperimentCode(String projectCode) {
243-
List<Experiment> experiments = searchExperimentsByCode(projectCode);
236+
List<Experiment> experiments = searchExperimentsByProjectCode(projectCode, new ExperimentFetchOptions());
244237
int lastExperimentNumber = 0;
245238
for (Experiment experiment : experiments) {
246239
lastExperimentNumber = Integer.max(lastExperimentNumber,
@@ -266,27 +259,103 @@ private void createOpenbisSamples(List<SampleCreation> samplesToRegister) {
266259
handleOperations(operation);
267260
}
268261

262+
private void updateOpenbisSamples(List<SampleUpdate> samplesToUpdate) {
263+
IOperation operation = new UpdateSamplesOperation(samplesToUpdate);
264+
handleOperations(operation);
265+
}
266+
269267
/**
270-
* Deletes a sample with the provided code from persistence.
268+
* Deletes a collection of samples with the provided codes from persistence. Checks if any of the
269+
* samples has attached data and fails the deletion of the sample batch, if so.
271270
*
272-
* @param sampleCode the {@link SampleCode} of the sample to delete
271+
* @param projectCode the {@link ProjectCode} of the project these samples belong to
272+
* @param sampleCodes The {@link SampleCode}s of the samples to be deleted in the data repo
273+
273274
* @since 1.0.0
274275
*/
275276
@Override
276-
public void delete(SampleCode sampleCode) {
277-
deleteOpenbisSample(DEFAULT_SPACE_CODE, sampleCode.code());
277+
public void deleteAll(ProjectCode projectCode,
278+
Collection<SampleCode> sampleCodes) {
279+
280+
Set<String> sampleCodesToDelete = new HashSet<>(
281+
sampleCodes.stream().map(SampleCode::code).toList());
282+
283+
//Fetch samples with potential data - sample search is not working, sorry you have to see this
284+
ExperimentFetchOptions fetchOptions = new ExperimentFetchOptions();
285+
fetchOptions.withSamplesUsing(fetchSamplesCompletely());
286+
for (Experiment experiment : searchExperimentsByProjectCode(projectCode.value(), fetchOptions)) {
287+
for (Sample sample : experiment.getSamples()) {
288+
String sampleCode = sample.getCode();
289+
if (sampleCodesToDelete.contains(sampleCode)) {
290+
if (isSampleWithData(List.of(sample))) {
291+
throw new SampleNotDeletedException(
292+
"Did not delete sample " + sampleCode + ", because data is attached.");
293+
}
294+
}
295+
}
296+
}
297+
// no data found, we can safely delete all samples
298+
sampleCodesToDelete.forEach(code -> deleteOpenbisSample(DEFAULT_SPACE_CODE, code));
299+
}
300+
301+
/**
302+
* Recursive method checking child samples for datasets
303+
*/
304+
private boolean isSampleWithData(List<Sample> samples) {
305+
boolean hasData = false;
306+
for (Sample sample : samples) {
307+
hasData |= !sample.getDataSets().isEmpty();
308+
hasData |= isSampleWithData(sample.getChildren());
309+
}
310+
return hasData;
278311
}
279312

280313
/**
281-
* Searches for samples that contain the provided sample code
314+
* Updates the reference to one or more {@link Sample}s in the data repository to connect project
315+
* data. Samples with metadata must be provided. Since no batch information is stored, changes in
316+
* the batch are not reflected.
282317
*
283-
* @param sampleCode the {@link SampleCode} to search for in the data repository
284-
* @return true, if a sample with that code already exists in the system, false if not
318+
* @param samples the batch of {@link Sample}s to be updated in the data repo
285319
* @since 1.0.0
286320
*/
287321
@Override
288-
public boolean sampleExists(SampleCode sampleCode) {
289-
return !searchSamplesByCode(sampleCode.code()).isEmpty();
322+
public void updateAll(
323+
Collection<life.qbic.projectmanagement.domain.model.sample.Sample> samples)
324+
throws SampleNotUpdatedException {
325+
try {
326+
/*FixMe Throws org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service
327+
and invalid stream header: 3C68746D
328+
*/
329+
// updateOpenbisSamples(convertSamplesToSampleUpdates(samples));
330+
} catch (RuntimeException e) {
331+
throw new SampleNotUpdatedException(
332+
"Samples could not be updated due to " + e.getCause() + " with " + e.getMessage());
333+
}
334+
}
335+
336+
private SampleUpdate createSampleUpdate(life.qbic.projectmanagement.domain.model.sample.Sample sample) {
337+
SampleUpdate sampleUpdate = new SampleUpdate();
338+
String sampleId = "/" + DEFAULT_SPACE_CODE + "/" + sample.sampleCode().code();
339+
sampleUpdate.setSampleId(new SampleIdentifier(sampleId));
340+
sampleUpdate.setProperty("Q_SECONDARY_NAME", sample.label());
341+
sampleUpdate.setProperty("Q_EXTERNALDB_ID", sample.sampleId().value());
342+
343+
String analyteValue = sample.sampleOrigin().getAnalyte().value();
344+
345+
String openBisSampleType = retrieveOpenBisAnalyteCode(analyteValue).or(
346+
() -> analyteMapper.mapFrom(analyteValue)).orElse(DEFAULT_ANALYTE_TYPE);
347+
sampleUpdate.setProperty("Q_SAMPLE_TYPE", openBisSampleType);
348+
if (openBisSampleType.equals(DEFAULT_ANALYTE_TYPE)) {
349+
logger("No mapping was found for " + analyteValue + " when updating sample.");
350+
logger("Using default value and adding " + analyteValue + " to Q_DETAILED_ANALYTE_TYPE.");
351+
sampleUpdate.setProperty("Q_DETAILED_ANALYTE_TYPE", analyteValue);
352+
}
353+
return sampleUpdate;
354+
}
355+
356+
private List<SampleUpdate> convertSamplesToSampleUpdates(
357+
Collection<life.qbic.projectmanagement.domain.model.sample.Sample> updatedSamples) {
358+
return updatedSamples.stream().map(this::createSampleUpdate).toList();
290359
}
291360

292361
record VocabularyTerm(String code, String label, String description) {
@@ -388,4 +457,18 @@ static class ConnectionException extends RuntimeException {
388457
static class MappingNotFoundException extends RuntimeException {
389458

390459
}
460+
461+
public static class SampleNotDeletedException extends RuntimeException {
462+
463+
public SampleNotDeletedException(String s) {
464+
super(s);
465+
}
466+
}
467+
468+
public static class SampleNotUpdatedException extends RuntimeException {
469+
470+
public SampleNotUpdatedException(String s) {
471+
super(s);
472+
}
473+
}
391474
}

0 commit comments

Comments
 (0)