Skip to content

Commit

Permalink
Merge branch 'feature/#680-move-from-the-spreadsheet-component-for-sa…
Browse files Browse the repository at this point in the history
…mple-batch-registration-to-xlsx-upload' of github.com:qbicsoftware/data-manager-app into feature/#680-move-from-the-spreadsheet-component-for-sample-batch-registration-to-xlsx-upload
  • Loading branch information
sven1103 committed Oct 8, 2024
2 parents 15ba06c + 3935ef0 commit 8efcb32
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,11 @@ public List<OntologyClass> query(String searchTerm, int offset, int limit)
@Override
public Optional<OntologyClass> searchByCurie(String curie) throws LookupException {
try {
List<TibTerm> result = searchByOboId(curie, 0, 10);
if (result.isEmpty()) {
return Optional.empty();
}
return Optional.of(
result.stream().map(TIBTerminologyServiceIntegration::convert).toList().get(0));
return searchByOboIdExact(curie).map(TIBTerminologyServiceIntegration::convert);
} catch (IOException e) {
throw wrapIO(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw wrapInterrupted(e);
} catch (Exception e) {
throw wrapUnknown(e);
Expand Down Expand Up @@ -283,16 +279,45 @@ private List<TibTerm> searchByOboId(String oboId, int offset, int limit)
HttpRequest termSelectQuery = HttpRequest.newBuilder().uri(URI.create(
searchEndpointAbsoluteUrl.toString() + "?q="
+ URLEncoder.encode(
oboId.replace("_", ":") + "&queryFields=obo_id",
//obo_id query field requires `:` separator instead of `_`
StandardCharsets.UTF_8)
oboId.replace("_", ":"), StandardCharsets.UTF_8)
+ "&queryFields=obo_id"
+ "&rows=" + limit + "&start=" + offset
+ "&ontology=" + createOntologyFilterQueryParameter()))
.header("Content-Type", "application/json").GET().build();
var response = HTTP_CLIENT.send(termSelectQuery, BodyHandlers.ofString());
return parseResponse(response);
}

/**
* Queries the /search endpoint of the TIB terminology service, but filters any results by the
* terms `obo_id` property.
* <p>
*
* @param oboId the obo id to match exactly
* @return a list of matching terms.
* @throws IOException if e.g. the service cannot be reached
* @throws InterruptedException the query is interrupted before succeeding
* @since 1.4.0
*/
private Optional<TibTerm> searchByOboIdExact(String oboId)
throws IOException, InterruptedException {
if (oboId.isBlank()) { // avoid unnecessary API calls
return Optional.empty();
}
HttpRequest termSelectQuery = HttpRequest.newBuilder().uri(URI.create(
searchEndpointAbsoluteUrl.toString() + "?q="
+ URLEncoder.encode(
//obo_id query field requires `:` separator instead of `_`
oboId.replace("_", ":"), StandardCharsets.UTF_8)
+ "&queryFields=obo_id"
+ "&exact=true"
+ "&ontology=" + createOntologyFilterQueryParameter()))
.header("Content-Type", "application/json").GET().build();
var response = HTTP_CLIENT.send(termSelectQuery, BodyHandlers.ofString());
return parseResponse(response).stream().findFirst();
}

/**
* Parses the TIB service response object and returns the wrapped terms.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,22 @@ public Result<Collection<Sample>, ResponseCode> addAll(Project project,
Collection<Sample> samples) {
String commaSeperatedSampleIds = buildCommaSeparatedSampleIds(
samples.stream().map(Sample::sampleId).toList());
List<Sample> savedSamples;
try {
this.qbicSampleRepository.saveAll(samples);
savedSamples = this.qbicSampleRepository.saveAll(samples);
} catch (Exception e) {
log.error("The samples:" + commaSeperatedSampleIds + "could not be saved", e);
return Result.fromError(ResponseCode.REGISTRATION_FAILED);
}
try {
sampleDataRepo.addSamplesToProject(project, samples.stream().toList());
sampleDataRepo.addSamplesToProject(project, savedSamples);
} catch (Exception e) {
log.error("The samples:" + commaSeperatedSampleIds + "could not be stored in openBIS", e);
log.error("Removing samples from repository, as well.");
qbicSampleRepository.deleteAll(samples);
qbicSampleRepository.deleteAll(savedSamples);
return Result.fromError(ResponseCode.REGISTRATION_FAILED);
}
return Result.fromValue(samples);
return Result.fromValue(savedSamples);
}

@Override
Expand Down Expand Up @@ -113,7 +114,7 @@ public void deleteAll(Project project,

@Override
public boolean isSampleRemovable(SampleId sampleId) {
SampleCode sampleCode = qbicSampleRepository.findById(sampleId).get().sampleCode();
SampleCode sampleCode = qbicSampleRepository.findById(sampleId).orElseThrow().sampleCode();
return sampleDataRepo.canDeleteSample(sampleCode);
}

Expand Down Expand Up @@ -147,6 +148,7 @@ public void updateAll(Project project,
sampleDataRepo.updateAll(project, updatedSamples);
}

@Transactional
@Override
public void updateAll(ProjectId projectId, Collection<Sample> updatedSamples) {
var projectQuery = projectRepository.find(projectId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ public Optional<Project> find(String projectId) throws IllegalArgumentException
return find(ProjectId.parse(projectId));
}

@PreAuthorize("hasPermission(#projectId,'life.qbic.projectmanagement.domain.model.project.Project','READ')")
public Optional<ProjectOverview> findOverview(ProjectId projectId) {
Objects.requireNonNull(projectId);
return projectOverviewLookup.query("", 0, 1, List.of(), List.of(projectId)).stream()
.findFirst();
}

public boolean isProjectCodeUnique(String projectCode) throws IllegalArgumentException {
return !projectRepository.existsProjectByProjectCode(ProjectCode.parse(projectCode));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import life.qbic.projectmanagement.application.DeletionService;
import life.qbic.projectmanagement.application.api.SampleCodeService;
import life.qbic.projectmanagement.application.batch.BatchRegistrationService;
import life.qbic.projectmanagement.domain.model.batch.BatchId;
Expand Down Expand Up @@ -37,13 +38,16 @@ public class SampleRegistrationServiceV2 {
private final BatchRegistrationService batchRegistrationService;
private final SampleRepository sampleRepository;
private final SampleCodeService sampleCodeService;
private final DeletionService deletionService;

@Autowired
public SampleRegistrationServiceV2(BatchRegistrationService batchRegistrationService,
SampleRepository sampleRepository, SampleCodeService sampleCodeService) {
SampleRepository sampleRepository, SampleCodeService sampleCodeService,
DeletionService deletionService) {
this.batchRegistrationService = Objects.requireNonNull(batchRegistrationService);
this.sampleRepository = Objects.requireNonNull(sampleRepository);
this.sampleCodeService = Objects.requireNonNull(sampleCodeService);
this.deletionService = Objects.requireNonNull(deletionService);
}

@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'WRITE')")
Expand All @@ -59,24 +63,29 @@ public CompletableFuture<Void> registerSamples(Collection<SampleMetadata> sample
try {
var sampleIds = registerSamples(sampleMetadata, batchId, projectId);
batchRegistrationService.addSamplesToBatch(sampleIds, batchId, projectId);
} catch (Exception e) {
} catch (RuntimeException e) {
rollbackSampleRegistration(batchId);
throw new RegistrationException("Sample batch registration failed");
deletionService.deleteSamples(projectId, batchId,
sampleMetadata.stream().map(SampleMetadata::sampleId).map(SampleId::parse).toList());
deletionService.deleteBatch(projectId, batchId);
throw e;
}
return CompletableFuture.completedFuture(null);
}

@PreAuthorize("hasPermission(#projectId, 'life.qbic.projectmanagement.domain.model.project.Project', 'WRITE')")
@Async
public CompletableFuture<Void> updateSamples(
Collection<SampleMetadata> sampleRegistrationRequests, ProjectId projectId) throws RegistrationException {
Collection<SampleMetadata> sampleRegistrationRequests, ProjectId projectId)
throws RegistrationException {
var samples = fetchSamples(
sampleRegistrationRequests.stream().map(SampleMetadata::sampleCode).map(SampleCode::create)
.toList());
var sampleBySampleCode = samples.stream().collect(Collectors.toMap(sample -> sample.sampleCode().code(), Function.identity()));
var sampleBySampleCode = samples.stream()
.collect(Collectors.toMap(sample -> sample.sampleCode().code(), Function.identity()));
var updatedSamples = updateSamples(sampleBySampleCode, sampleRegistrationRequests);
sampleRepository.updateAll(projectId, updatedSamples);
return CompletableFuture.completedFuture( null);
return CompletableFuture.completedFuture(null);
}

private List<Sample> updateSamples(Map<String, Sample> samples,
Expand All @@ -85,7 +94,8 @@ private List<Sample> updateSamples(Map<String, Sample> samples,
for (SampleMetadata sampleMetadata : sampleRegistrationRequests) {
var sampleForUpdate = samples.get(sampleMetadata.sampleCode());
sampleForUpdate.setLabel(sampleMetadata.sampleName());
var sampleOrigin = SampleOrigin.create(sampleMetadata.species(), sampleMetadata.specimen(), sampleMetadata.analyte());
var sampleOrigin = SampleOrigin.create(sampleMetadata.species(), sampleMetadata.specimen(),
sampleMetadata.analyte());
sampleForUpdate.setSampleOrigin(sampleOrigin);
sampleForUpdate.setExperimentalGroupId(sampleMetadata.experimentalGroupId());
sampleForUpdate.setAnalysisMethod(sampleMetadata.analysisToBePerformed());
Expand All @@ -108,15 +118,19 @@ private Sample fetchSample(SampleCode sampleCode) throws UnknownSampleException
return sampleQuery.get();
}

private Collection<SampleId> registerSamples(Collection<SampleMetadata> sampleMetadata, BatchId batchId,
private Collection<SampleId> registerSamples(Collection<SampleMetadata> sampleMetadata,
BatchId batchId,
ProjectId projectId)
throws RegistrationException {
var samplesToRegister = new ArrayList<Sample>();
var sampleCodes = generateSampleCodes(sampleMetadata.size(), projectId).iterator();
for (SampleMetadata sample : sampleMetadata) {
samplesToRegister.add(buildSample(sample, batchId, sampleCodes.next()));
}
return sampleRepository.addAll(projectId, samplesToRegister).getValue().stream().map(Sample::sampleId).toList();
return sampleRepository.addAll(projectId, samplesToRegister)
.valueOrElseThrow(e -> new RegistrationException("Could not register samples: " + e.name()))
.stream().map(Sample::sampleId)
.toList();
}

private Sample buildSample(SampleMetadata sample, BatchId batchId, SampleCode sampleCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@
*/
public enum RegisterColumn {

ANALYSIS("Analysis to be performed", 0, false),
SAMPLE_NAME("Sample Name", 1, false),
BIOLOGICAL_REPLICATE("Biological Replicate", 2, false),
CONDITION("Condition", 3, false),
SPECIES("Species", 4, false),
ANALYTE("Analyte", 5, false),
SPECIMEN("Specimen", 6, false),
COMMENT("Comment", 7, false);
ANALYSIS("Analysis to be performed", 0, false, true),
SAMPLE_NAME("Sample Name", 1, false, true),
BIOLOGICAL_REPLICATE("Biological Replicate", 2, false, false),
CONDITION("Condition", 3, false, true),
SPECIES("Species", 4, false, true),
ANALYTE("Analyte", 5, false, true),
SPECIMEN("Specimen", 6, false, true),
COMMENT("Comment", 7, false, false);

private final String headerName;
private final int columnIndex;
private final boolean readOnly;
private final boolean mandatory;

public static int maxColumnIndex() {
return Arrays.stream(values())
Expand All @@ -35,11 +36,13 @@ public static int maxColumnIndex() {
* @param headerName the name in the header
* @param columnIndex the index of the column this property is in
* @param readOnly is the property read only
* @param mandatory
*/
RegisterColumn(String headerName, int columnIndex, boolean readOnly) {
RegisterColumn(String headerName, int columnIndex, boolean readOnly, boolean mandatory) {
this.headerName = headerName;
this.columnIndex = columnIndex;
this.readOnly = readOnly;
this.mandatory = mandatory;
}

public String headerName() {
Expand All @@ -50,8 +53,11 @@ public int columnIndex() {
return columnIndex;
}

public boolean readOnly() {
public boolean isReadOnly() {
return readOnly;
}

public boolean isMandatory() {
return mandatory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ public static XSSFWorkbook createRegistrationTemplate(List<String> conditions,
Row header = getOrCreateRow(sheet, 0);
for (RegisterColumn column : RegisterColumn.values()) {
var cell = XLSXTemplateHelper.getOrCreateCell(header, column.columnIndex());
cell.setCellValue(column.headerName());

cell.setCellStyle(boldCellStyle);
if (column.readOnly()) {
if (column.isMandatory()) {
cell.setCellValue(column.headerName() + "*");
} else {
cell.setCellValue(column.headerName());
}
if (column.isReadOnly()) {
cell.setCellStyle(readOnlyHeaderStyle);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import life.qbic.logging.service.LoggerFactory;
import life.qbic.projectmanagement.application.DeletionService;
import life.qbic.projectmanagement.application.ProjectInformationService;
import life.qbic.projectmanagement.application.ProjectOverview;
import life.qbic.projectmanagement.application.batch.BatchRegistrationService;
import life.qbic.projectmanagement.application.batch.SampleUpdateRequest;
import life.qbic.projectmanagement.application.batch.SampleUpdateRequest.SampleInformation;
Expand Down Expand Up @@ -222,30 +223,38 @@ private void downloadSampleMetadata() {
}

private void onRegisterBatchClicked() {
Experiment experiment = context.experimentId()
.flatMap(
id -> experimentInformationService.find(context.projectId().orElseThrow().value(), id))
ProjectId projectId = context.projectId().orElseThrow();
ExperimentId experimentId = context.experimentId().orElseThrow();

Experiment experiment = experimentInformationService.find(projectId.value(), experimentId)
.orElseThrow();

if (experiment.getExperimentalGroups().isEmpty()) {
return;
}
ProjectOverview projectOverview = projectInformationService.findOverview(projectId)
.orElseThrow();
RegisterSampleBatchDialog registerSampleBatchDialog = new RegisterSampleBatchDialog(
sampleValidationService, templateService,
context.experimentId().map(ExperimentId::value).orElseThrow(),
context.projectId().map(ProjectId::value).orElseThrow());
sampleValidationService, templateService, experimentId.value(),
projectId.value(), projectOverview.projectCode());
registerSampleBatchDialog.addConfirmListener(event -> {
event.getSource().taskInProgress("Register the sample batch metadata",
"It may take about a minute for the registration task to complete.");
UI ui = event.getSource().getUI().orElseThrow();
CompletableFuture<Void> registrationTask = sampleRegistrationServiceV2.registerSamples(
event.validatedSampleMetadata(),
context.projectId()
.orElseThrow(), event.batchName(), event.isPilot())
projectId, event.batchName(), false)
.orTimeout(5, TimeUnit.MINUTES);
registrationTask
.thenRun(() -> ui.access(() -> event.getSource().close()))
.exceptionally(e -> null /*TODO show failed*/);
event.getSource().close();
.thenRun(() -> ui.access(() -> {
event.getSource().taskSucceeded("", ""); //todo label and description
}))
.exceptionally(e -> {
ui.access(() -> {
event.getSource().taskFailed("", ""); //todo label and description s
});
return null;
});
});
registerSampleBatchDialog.addCancelListener(
event -> showCancelConfirmationDialog(event.getSource()));
Expand Down
Loading

0 comments on commit 8efcb32

Please sign in to comment.